blob: 14cac0be95ed6e980facab3edc99722a2b0ffb59 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
SESSION_TODO_CONTENT_MAX_CHARS,
SESSION_TODO_MAX_ITEMS,
SESSION_TODO_STATUSES,
sessionTodoContentForDisplay,
type SessionTodoSnapshot,
} from '@maka/core/session-todo';
import { z } from 'zod';
import type { MakaTool } from './tool-runtime.js';
export const TODO_READ_TOOL_NAME = 'todo_read';
export const TODO_WRITE_TOOL_NAME = 'todo_write';
export interface SessionTodoToolStore {
read(sessionId: string): Promise<SessionTodoSnapshot>;
replace(sessionId: string, items: unknown): Promise<SessionTodoSnapshot>;
}
export function buildSessionTodoTools(store: SessionTodoToolStore): MakaTool[] {
return [buildTodoReadTool(store), buildTodoWriteTool(store)];
}
function buildTodoReadTool(store: SessionTodoToolStore): MakaTool<Record<string, never>, string> {
return {
name: TODO_READ_TOOL_NAME,
displayName: 'Todo Read',
description:
'Read the complete current session Todo list. Use this when you need the latest checklist.',
parameters: z.object({}).strict(),
impl: async (_input, ctx) => renderTodoSnapshot(await store.read(ctx.sessionId), 'read'),
};
}
function buildTodoWriteTool(
store: SessionTodoToolStore,
): MakaTool<
{ todos: Array<{ content: string; status: (typeof SESSION_TODO_STATUSES)[number] }> },
string
> {
return {
name: TODO_WRITE_TOOL_NAME,
displayName: 'Todo Write',
description:
'Atomically replace the complete current session Todo list. Include every item that should remain. ' +
'Completed means model-reported progress; it is not independently verified execution evidence.',
parameters: z
.object({
todos: z
.array(
z
.object({
content: z
.string()
.trim()
.min(1)
.refine(
(content) =>
[...content.normalize('NFC')].length <= SESSION_TODO_CONTENT_MAX_CHARS,
`Todo content must be ${SESSION_TODO_CONTENT_MAX_CHARS} characters or fewer`,
),
status: z.enum(SESSION_TODO_STATUSES),
})
.strict(),
)
.max(SESSION_TODO_MAX_ITEMS),
})
.strict(),
impl: async (input, ctx) =>
renderTodoSnapshot(await store.replace(ctx.sessionId, input.todos), 'write'),
};
}
function renderTodoSnapshot(snapshot: SessionTodoSnapshot, operation: 'read' | 'write'): string {
if (snapshot.items.length === 0) {
return operation === 'write' ? 'Todo list cleared.' : 'Todo list is empty.';
}
const lines = snapshot.items.map(
(item, index) =>
`${index + 1}. [${item.status}] ${JSON.stringify(sessionTodoContentForDisplay(item.content))}`,
);
const prefix = operation === 'write' ? 'Todo list updated' : 'Todo list';
return `${prefix} (${snapshot.items.length} items):\n${lines.join('\n')}`;
}