zeitlich 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +165 -180
- package/dist/index.cjs +1314 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +128 -0
- package/dist/index.d.ts +51 -75
- package/dist/index.js +741 -1091
- package/dist/index.js.map +1 -1
- package/dist/workflow-uVNF7zoe.d.cts +941 -0
- package/dist/workflow-uVNF7zoe.d.ts +941 -0
- package/dist/workflow.cjs +914 -0
- package/dist/workflow.cjs.map +1 -0
- package/dist/workflow.d.cts +5 -0
- package/dist/workflow.d.ts +2 -1
- package/dist/workflow.js +543 -423
- package/dist/workflow.js.map +1 -1
- package/package.json +19 -17
- package/src/activities.ts +112 -0
- package/src/index.ts +49 -0
- package/src/lib/fs.ts +80 -0
- package/src/lib/model-invoker.ts +75 -0
- package/src/lib/session.ts +216 -0
- package/src/lib/state-manager.ts +268 -0
- package/src/lib/thread-manager.ts +169 -0
- package/src/lib/tool-router.ts +717 -0
- package/src/lib/types.ts +354 -0
- package/src/plugin.ts +28 -0
- package/src/tools/ask-user-question/handler.ts +25 -0
- package/src/tools/ask-user-question/tool.ts +46 -0
- package/src/tools/bash/bash.test.ts +104 -0
- package/src/tools/bash/handler.ts +36 -0
- package/src/tools/bash/tool.ts +20 -0
- package/src/tools/edit/handler.ts +156 -0
- package/src/tools/edit/tool.ts +39 -0
- package/src/tools/glob/handler.ts +62 -0
- package/src/tools/glob/tool.ts +27 -0
- package/src/tools/grep/tool.ts +45 -0
- package/src/tools/read/tool.ts +33 -0
- package/src/tools/task/handler.ts +75 -0
- package/src/tools/task/tool.ts +96 -0
- package/src/tools/task-create/handler.ts +49 -0
- package/src/tools/task-create/tool.ts +66 -0
- package/src/tools/task-get/handler.ts +38 -0
- package/src/tools/task-get/tool.ts +11 -0
- package/src/tools/task-list/handler.ts +33 -0
- package/src/tools/task-list/tool.ts +9 -0
- package/src/tools/task-update/handler.ts +79 -0
- package/src/tools/task-update/tool.ts +20 -0
- package/src/tools/write/tool.ts +26 -0
- package/src/workflow.ts +138 -0
- package/tsup.config.ts +20 -0
- package/dist/index.d.mts +0 -152
- package/dist/index.mjs +0 -1587
- package/dist/index.mjs.map +0 -1
- package/dist/workflow-7_MT-5-w.d.mts +0 -1203
- package/dist/workflow-7_MT-5-w.d.ts +0 -1203
- package/dist/workflow.d.mts +0 -4
- package/dist/workflow.mjs +0 -739
- package/dist/workflow.mjs.map +0 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type Redis from "ioredis";
|
|
2
|
+
import { createThreadManager } from "./lib/thread-manager";
|
|
3
|
+
import type { ToolResultConfig } from "./lib/types";
|
|
4
|
+
import {
|
|
5
|
+
type AIMessage,
|
|
6
|
+
mapStoredMessageToChatMessage,
|
|
7
|
+
type MessageContent,
|
|
8
|
+
type StoredMessage,
|
|
9
|
+
} from "@langchain/core/messages";
|
|
10
|
+
import type { RawToolCall } from "./lib/tool-router";
|
|
11
|
+
/**
|
|
12
|
+
* Shared Zeitlich activities - thread management and message handling
|
|
13
|
+
* Note: runAgent is workflow-specific and should be created per-workflow
|
|
14
|
+
*/
|
|
15
|
+
export interface ZeitlichSharedActivities {
|
|
16
|
+
appendSystemMessage(threadId: string, content: string): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Append a tool result to the thread.
|
|
19
|
+
* Handles JSON serialization and optional cache points.
|
|
20
|
+
*/
|
|
21
|
+
appendToolResult(config: ToolResultConfig): Promise<void>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Initialize an empty thread.
|
|
25
|
+
*/
|
|
26
|
+
initializeThread(threadId: string): Promise<void>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Append a system message to a thread.
|
|
30
|
+
*/
|
|
31
|
+
appendSystemMessage(threadId: string, content: string): Promise<void>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Append messages to a thread.
|
|
35
|
+
*/
|
|
36
|
+
appendThreadMessages(
|
|
37
|
+
threadId: string,
|
|
38
|
+
messages: StoredMessage[]
|
|
39
|
+
): Promise<void>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Append a human message to a thread.
|
|
43
|
+
*/
|
|
44
|
+
appendHumanMessage(
|
|
45
|
+
threadId: string,
|
|
46
|
+
content: string | MessageContent
|
|
47
|
+
): Promise<void>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract raw tool calls from a stored message.
|
|
51
|
+
* Returns unvalidated tool calls - use toolRegistry.parseToolCall() to validate.
|
|
52
|
+
*/
|
|
53
|
+
parseToolCalls(storedMessage: StoredMessage): Promise<RawToolCall[]>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Creates shared Temporal activities for thread management
|
|
58
|
+
*
|
|
59
|
+
* @returns An object containing the shared activity functions
|
|
60
|
+
*
|
|
61
|
+
* @experimental The Zeitlich integration is an experimental feature; APIs may change without notice.
|
|
62
|
+
*/
|
|
63
|
+
export function createSharedActivities(redis: Redis): ZeitlichSharedActivities {
|
|
64
|
+
return {
|
|
65
|
+
async appendSystemMessage(
|
|
66
|
+
threadId: string,
|
|
67
|
+
content: string
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
const thread = createThreadManager({ redis, threadId });
|
|
70
|
+
await thread.appendSystemMessage(content);
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async appendToolResult(config: ToolResultConfig): Promise<void> {
|
|
74
|
+
const { threadId, toolCallId, content } = config;
|
|
75
|
+
const thread = createThreadManager({ redis, threadId });
|
|
76
|
+
|
|
77
|
+
await thread.appendToolMessage(content, toolCallId);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
async initializeThread(threadId: string): Promise<void> {
|
|
81
|
+
const thread = createThreadManager({ redis, threadId });
|
|
82
|
+
await thread.initialize();
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
async appendThreadMessages(
|
|
86
|
+
threadId: string,
|
|
87
|
+
messages: StoredMessage[]
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
const thread = createThreadManager({ redis, threadId });
|
|
90
|
+
await thread.append(messages);
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
async appendHumanMessage(
|
|
94
|
+
threadId: string,
|
|
95
|
+
content: string | MessageContent
|
|
96
|
+
): Promise<void> {
|
|
97
|
+
const thread = createThreadManager({ redis, threadId });
|
|
98
|
+
await thread.appendHumanMessage(content);
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
async parseToolCalls(storedMessage: StoredMessage): Promise<RawToolCall[]> {
|
|
102
|
+
const message = mapStoredMessageToChatMessage(storedMessage) as AIMessage;
|
|
103
|
+
const toolCalls = message.tool_calls ?? [];
|
|
104
|
+
|
|
105
|
+
return toolCalls.map((toolCall) => ({
|
|
106
|
+
id: toolCall.id,
|
|
107
|
+
name: toolCall.name,
|
|
108
|
+
args: toolCall.args,
|
|
109
|
+
}));
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity-side exports for use in Temporal activity code and worker setup.
|
|
3
|
+
*
|
|
4
|
+
* Import from '@bead-ai/zeitlich' in activity files and worker setup.
|
|
5
|
+
* These exports may have external dependencies (Redis, LangChain).
|
|
6
|
+
*
|
|
7
|
+
* For workflow code, use '@bead-ai/zeitlich/workflow' instead.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* // In your activities file
|
|
12
|
+
* import { invokeModel, globHandler } from '@bead-ai/zeitlich';
|
|
13
|
+
*
|
|
14
|
+
* // In your worker file
|
|
15
|
+
* import { ZeitlichPlugin } from '@bead-ai/zeitlich';
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Re-export all workflow-safe exports for convenience
|
|
20
|
+
// (Activities can use these too)
|
|
21
|
+
export * from "./workflow";
|
|
22
|
+
|
|
23
|
+
// Plugin (requires Redis)
|
|
24
|
+
export { ZeitlichPlugin } from "./plugin";
|
|
25
|
+
export type { ZeitlichPluginOptions } from "./plugin";
|
|
26
|
+
|
|
27
|
+
// Shared activities (requires Redis)
|
|
28
|
+
export { createSharedActivities } from "./activities";
|
|
29
|
+
export type { ZeitlichSharedActivities } from "./activities";
|
|
30
|
+
|
|
31
|
+
// Model invocation (requires Redis, LangChain)
|
|
32
|
+
export { invokeModel } from "./lib/model-invoker";
|
|
33
|
+
export type { InvokeModelConfig } from "./lib/model-invoker";
|
|
34
|
+
|
|
35
|
+
// Tool handlers (activity implementations)
|
|
36
|
+
// These are direct functions that accept scopedNodes per-call for dynamic file trees
|
|
37
|
+
export { handleAskUserQuestionToolResult } from "./tools/ask-user-question/handler";
|
|
38
|
+
export { globHandler } from "./tools/glob/handler";
|
|
39
|
+
|
|
40
|
+
export { editHandler } from "./tools/edit/handler";
|
|
41
|
+
export type {
|
|
42
|
+
EditResult,
|
|
43
|
+
EditHandlerResponse,
|
|
44
|
+
EditHandlerOptions,
|
|
45
|
+
} from "./tools/edit/handler";
|
|
46
|
+
|
|
47
|
+
export { handleBashTool } from "./tools/bash/handler";
|
|
48
|
+
|
|
49
|
+
export { toTree } from "./lib/fs";
|
package/src/lib/fs.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { IFileSystem } from "just-bash";
|
|
2
|
+
|
|
3
|
+
const basename = (path: string, separator: string): string => {
|
|
4
|
+
if (path[path.length - 1] === separator) path = path.slice(0, -1);
|
|
5
|
+
const lastSlashIndex = path.lastIndexOf(separator);
|
|
6
|
+
return lastSlashIndex === -1 ? path : path.slice(lastSlashIndex + 1);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const printTree = async (
|
|
10
|
+
tab = "",
|
|
11
|
+
children: ((tab: string) => Promise<string | null>)[]
|
|
12
|
+
): Promise<string> => {
|
|
13
|
+
let str = "";
|
|
14
|
+
let last = children.length - 1;
|
|
15
|
+
for (; last >= 0; last--) if (children[last]) break;
|
|
16
|
+
for (let i = 0; i <= last; i++) {
|
|
17
|
+
const fn = children[i];
|
|
18
|
+
if (!fn) continue;
|
|
19
|
+
const isLast = i === last;
|
|
20
|
+
const child = await fn(tab + (isLast ? " " : "│") + " ");
|
|
21
|
+
const branch = child ? (isLast ? "└─" : "├─") : "│";
|
|
22
|
+
str += "\n" + tab + branch + (child ? " " + child : "");
|
|
23
|
+
}
|
|
24
|
+
return str;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const toTree = async (
|
|
28
|
+
fs: IFileSystem,
|
|
29
|
+
opts: {
|
|
30
|
+
dir?: string;
|
|
31
|
+
separator?: "/" | "\\";
|
|
32
|
+
depth?: number;
|
|
33
|
+
tab?: string;
|
|
34
|
+
sort?: boolean;
|
|
35
|
+
} = {}
|
|
36
|
+
): Promise<string> => {
|
|
37
|
+
const separator = opts.separator || "/";
|
|
38
|
+
let dir = opts.dir || separator;
|
|
39
|
+
if (dir[dir.length - 1] !== separator) dir += separator;
|
|
40
|
+
const tab = opts.tab || "";
|
|
41
|
+
const depth = opts.depth ?? 10;
|
|
42
|
+
const sort = opts.sort ?? true;
|
|
43
|
+
let subtree = " (...)";
|
|
44
|
+
if (depth > 0) {
|
|
45
|
+
const list = (await fs.readdirWithFileTypes?.(dir)) || [];
|
|
46
|
+
if (sort) {
|
|
47
|
+
list.sort((a, b) => {
|
|
48
|
+
if (a.isDirectory && b.isDirectory) {
|
|
49
|
+
return a.name.toString().localeCompare(b.name.toString());
|
|
50
|
+
} else if (a.isDirectory) {
|
|
51
|
+
return -1;
|
|
52
|
+
} else if (b.isDirectory) {
|
|
53
|
+
return 1;
|
|
54
|
+
} else {
|
|
55
|
+
return a.name.toString().localeCompare(b.name.toString());
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
subtree = await printTree(
|
|
60
|
+
tab,
|
|
61
|
+
list.map((entry) => async (tab): Promise<string | null> => {
|
|
62
|
+
if (entry.isDirectory) {
|
|
63
|
+
return toTree(fs, {
|
|
64
|
+
dir: dir + entry.name,
|
|
65
|
+
depth: depth - 1,
|
|
66
|
+
tab,
|
|
67
|
+
});
|
|
68
|
+
} else if (entry.isSymbolicLink) {
|
|
69
|
+
return (
|
|
70
|
+
"" + entry.name + " → " + (await fs.readlink(dir + entry.name))
|
|
71
|
+
);
|
|
72
|
+
} else {
|
|
73
|
+
return "" + entry.name;
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const base = basename(dir, separator) + separator;
|
|
79
|
+
return base + subtree;
|
|
80
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type Redis from "ioredis";
|
|
2
|
+
import { createThreadManager } from "./thread-manager";
|
|
3
|
+
import type { AgentResponse } from "./types";
|
|
4
|
+
import { Context } from "@temporalio/activity";
|
|
5
|
+
import type { WorkflowClient } from "@temporalio/client";
|
|
6
|
+
import { mapStoredMessagesToChatMessages } from "@langchain/core/messages";
|
|
7
|
+
import { v4 as uuidv4 } from "uuid";
|
|
8
|
+
import type {
|
|
9
|
+
BaseChatModel,
|
|
10
|
+
BaseChatModelCallOptions,
|
|
11
|
+
BindToolsInput,
|
|
12
|
+
} from "@langchain/core/language_models/chat_models";
|
|
13
|
+
import { getStateQuery } from "./state-manager";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Configuration for invoking the model
|
|
17
|
+
*/
|
|
18
|
+
export interface InvokeModelConfig {
|
|
19
|
+
threadId: string;
|
|
20
|
+
agentName: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Core model invocation logic - shared utility for workflow-specific activities
|
|
25
|
+
*
|
|
26
|
+
* @param redis - Redis client for thread management
|
|
27
|
+
* @param config - Model invocation configuration
|
|
28
|
+
* @param model - Pre-instantiated LangChain chat model
|
|
29
|
+
* @param invocationConfig - Per-invocation configuration (system prompt, etc.)
|
|
30
|
+
* @returns Agent response with message and metadata
|
|
31
|
+
*/
|
|
32
|
+
export async function invokeModel({
|
|
33
|
+
redis,
|
|
34
|
+
model,
|
|
35
|
+
client,
|
|
36
|
+
config: { threadId, agentName },
|
|
37
|
+
}: {
|
|
38
|
+
redis: Redis;
|
|
39
|
+
client: WorkflowClient;
|
|
40
|
+
config: InvokeModelConfig;
|
|
41
|
+
model: BaseChatModel<BaseChatModelCallOptions & { tools?: BindToolsInput }>;
|
|
42
|
+
}): Promise<AgentResponse> {
|
|
43
|
+
const thread = createThreadManager({ redis, threadId });
|
|
44
|
+
const runId = uuidv4();
|
|
45
|
+
|
|
46
|
+
const info = Context.current().info; // Activity info
|
|
47
|
+
const parentWorkflowId = info.workflowExecution.workflowId;
|
|
48
|
+
const parentRunId = info.workflowExecution.runId;
|
|
49
|
+
|
|
50
|
+
const handle = client.getHandle(parentWorkflowId, parentRunId);
|
|
51
|
+
const { tools } = await handle.query(getStateQuery);
|
|
52
|
+
|
|
53
|
+
const messages = await thread.load();
|
|
54
|
+
const response = await model.invoke(
|
|
55
|
+
[...mapStoredMessagesToChatMessages(messages)],
|
|
56
|
+
{
|
|
57
|
+
runName: agentName,
|
|
58
|
+
runId,
|
|
59
|
+
metadata: { thread_id: threadId },
|
|
60
|
+
tools,
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
await thread.append([response.toDict()]);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
message: response.toDict(),
|
|
68
|
+
stopReason: (response.response_metadata?.stop_reason as string) ?? null,
|
|
69
|
+
usage: {
|
|
70
|
+
input_tokens: response.usage_metadata?.input_tokens,
|
|
71
|
+
output_tokens: response.usage_metadata?.output_tokens,
|
|
72
|
+
total_tokens: response.usage_metadata?.total_tokens,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { proxyActivities } from "@temporalio/workflow";
|
|
2
|
+
import type { ZeitlichSharedActivities } from "../activities";
|
|
3
|
+
import type {
|
|
4
|
+
ZeitlichAgentConfig,
|
|
5
|
+
SessionStartHook,
|
|
6
|
+
SessionEndHook,
|
|
7
|
+
SessionExitReason,
|
|
8
|
+
SubagentConfig,
|
|
9
|
+
} from "./types";
|
|
10
|
+
import { type AgentStateManager, type JsonSerializable } from "./state-manager";
|
|
11
|
+
import {
|
|
12
|
+
createToolRouter,
|
|
13
|
+
type ParsedToolCall,
|
|
14
|
+
type ParsedToolCallUnion,
|
|
15
|
+
type RawToolCall,
|
|
16
|
+
type ToolMap,
|
|
17
|
+
} from "./tool-router";
|
|
18
|
+
import type { StoredMessage } from "@langchain/core/messages";
|
|
19
|
+
import { createTaskTool, type TaskToolSchemaType } from "../tools/task/tool";
|
|
20
|
+
|
|
21
|
+
export interface ZeitlichSession {
|
|
22
|
+
runSession<T extends JsonSerializable<T>>(args: {
|
|
23
|
+
stateManager: AgentStateManager<T>;
|
|
24
|
+
}): Promise<StoredMessage | null>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function resolvePrompt(
|
|
28
|
+
prompt: string | (() => string | Promise<string>)
|
|
29
|
+
): Promise<string> {
|
|
30
|
+
if (typeof prompt === "function") {
|
|
31
|
+
return prompt();
|
|
32
|
+
}
|
|
33
|
+
return prompt;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Session-level hooks for lifecycle events
|
|
38
|
+
*/
|
|
39
|
+
export interface SessionLifecycleHooks {
|
|
40
|
+
/** Called when session starts */
|
|
41
|
+
onSessionStart?: SessionStartHook;
|
|
42
|
+
/** Called when session ends */
|
|
43
|
+
onSessionEnd?: SessionEndHook;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const createSession = async <T extends ToolMap>({
|
|
47
|
+
threadId,
|
|
48
|
+
agentName,
|
|
49
|
+
maxTurns = 50,
|
|
50
|
+
metadata = {},
|
|
51
|
+
runAgent,
|
|
52
|
+
baseSystemPrompt,
|
|
53
|
+
instructionsPrompt,
|
|
54
|
+
buildContextMessage,
|
|
55
|
+
buildFileTree = async (): Promise<string> => "",
|
|
56
|
+
subagents,
|
|
57
|
+
tools = {} as T,
|
|
58
|
+
processToolsInParallel = true,
|
|
59
|
+
buildInTools = {},
|
|
60
|
+
hooks = {},
|
|
61
|
+
}: ZeitlichAgentConfig<T>): Promise<ZeitlichSession> => {
|
|
62
|
+
const {
|
|
63
|
+
initializeThread,
|
|
64
|
+
appendHumanMessage,
|
|
65
|
+
parseToolCalls,
|
|
66
|
+
appendToolResult,
|
|
67
|
+
appendSystemMessage,
|
|
68
|
+
} = proxyActivities<ZeitlichSharedActivities>({
|
|
69
|
+
startToCloseTimeout: "30m",
|
|
70
|
+
retry: {
|
|
71
|
+
maximumAttempts: 6,
|
|
72
|
+
initialInterval: "5s",
|
|
73
|
+
maximumInterval: "15m",
|
|
74
|
+
backoffCoefficient: 4,
|
|
75
|
+
},
|
|
76
|
+
heartbeatTimeout: "5m",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const fileTree = await buildFileTree();
|
|
80
|
+
|
|
81
|
+
const toolRouter = createToolRouter({
|
|
82
|
+
tools,
|
|
83
|
+
appendToolResult,
|
|
84
|
+
threadId,
|
|
85
|
+
hooks,
|
|
86
|
+
buildInTools,
|
|
87
|
+
fileTree,
|
|
88
|
+
subagents,
|
|
89
|
+
parallel: processToolsInParallel,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Helper to call session end hook
|
|
93
|
+
const callSessionEnd = async (
|
|
94
|
+
exitReason: SessionExitReason,
|
|
95
|
+
turns: number
|
|
96
|
+
): Promise<void> => {
|
|
97
|
+
if (hooks.onSessionEnd) {
|
|
98
|
+
await hooks.onSessionEnd({
|
|
99
|
+
threadId,
|
|
100
|
+
agentName,
|
|
101
|
+
exitReason,
|
|
102
|
+
turns,
|
|
103
|
+
metadata,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
runSession: async ({ stateManager }): Promise<StoredMessage | null> => {
|
|
110
|
+
if (hooks.onSessionStart) {
|
|
111
|
+
await hooks.onSessionStart({
|
|
112
|
+
threadId,
|
|
113
|
+
agentName,
|
|
114
|
+
metadata,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
stateManager.setTools(toolRouter.getToolDefinitions());
|
|
119
|
+
|
|
120
|
+
await initializeThread(threadId);
|
|
121
|
+
await appendSystemMessage(
|
|
122
|
+
threadId,
|
|
123
|
+
[
|
|
124
|
+
await resolvePrompt(baseSystemPrompt),
|
|
125
|
+
await resolvePrompt(instructionsPrompt),
|
|
126
|
+
].join("\n")
|
|
127
|
+
);
|
|
128
|
+
await appendHumanMessage(threadId, await buildContextMessage());
|
|
129
|
+
|
|
130
|
+
let exitReason: SessionExitReason = "completed";
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
while (
|
|
134
|
+
stateManager.isRunning() &&
|
|
135
|
+
!stateManager.isTerminal() &&
|
|
136
|
+
stateManager.getTurns() < maxTurns
|
|
137
|
+
) {
|
|
138
|
+
stateManager.incrementTurns();
|
|
139
|
+
const currentTurn = stateManager.getTurns();
|
|
140
|
+
|
|
141
|
+
const { message, stopReason } = await runAgent({
|
|
142
|
+
threadId,
|
|
143
|
+
agentName,
|
|
144
|
+
metadata,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (stopReason === "end_turn") {
|
|
148
|
+
stateManager.complete();
|
|
149
|
+
exitReason = "completed";
|
|
150
|
+
return message;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// No tools configured - treat any non-end_turn as completed
|
|
154
|
+
if (!toolRouter.hasTools()) {
|
|
155
|
+
stateManager.complete();
|
|
156
|
+
exitReason = "completed";
|
|
157
|
+
return message;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const rawToolCalls: RawToolCall[] = await parseToolCalls(message);
|
|
161
|
+
const parsedToolCalls = rawToolCalls
|
|
162
|
+
.filter((tc: RawToolCall) => tc.name !== "Task")
|
|
163
|
+
.map((tc: RawToolCall) => toolRouter.parseToolCall(tc));
|
|
164
|
+
const taskToolCalls =
|
|
165
|
+
subagents && subagents.length > 0
|
|
166
|
+
? rawToolCalls
|
|
167
|
+
.filter((tc: RawToolCall) => tc.name === "Task")
|
|
168
|
+
.map((tc: RawToolCall) => {
|
|
169
|
+
// Parse and validate args using the tool's schema
|
|
170
|
+
const parsedArgs = createTaskTool(subagents).schema.parse(
|
|
171
|
+
tc.args
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
id: tc.id ?? "",
|
|
176
|
+
name: tc.name,
|
|
177
|
+
args: parsedArgs,
|
|
178
|
+
} as ParsedToolCall<
|
|
179
|
+
"Task",
|
|
180
|
+
TaskToolSchemaType<SubagentConfig[]>
|
|
181
|
+
>;
|
|
182
|
+
})
|
|
183
|
+
: [];
|
|
184
|
+
|
|
185
|
+
// Hooks can call stateManager.waitForInput() to pause the session
|
|
186
|
+
await toolRouter.processToolCalls(
|
|
187
|
+
[...parsedToolCalls, ...taskToolCalls] as ParsedToolCallUnion<
|
|
188
|
+
T & { Task: TaskToolSchemaType<SubagentConfig[]> }
|
|
189
|
+
>[],
|
|
190
|
+
{
|
|
191
|
+
turn: currentTurn,
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
if (stateManager.getStatus() === "WAITING_FOR_INPUT") {
|
|
196
|
+
exitReason = "waiting_for_input";
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check if we hit max turns
|
|
202
|
+
if (stateManager.getTurns() >= maxTurns && stateManager.isRunning()) {
|
|
203
|
+
exitReason = "max_turns";
|
|
204
|
+
}
|
|
205
|
+
} catch (error) {
|
|
206
|
+
exitReason = "failed";
|
|
207
|
+
throw error;
|
|
208
|
+
} finally {
|
|
209
|
+
// SessionEnd hook - always called
|
|
210
|
+
await callSessionEnd(exitReason, stateManager.getTurns());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return null;
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
};
|