zeitlich 0.2.7 → 0.2.9

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.
@@ -24,7 +24,11 @@ import type { MessageContent } from "@langchain/core/messages";
24
24
  export interface ZeitlichSession<M = unknown> {
25
25
  runSession<T extends JsonSerializable<T>>(args: {
26
26
  stateManager: AgentStateManager<T>;
27
- }): Promise<M | null>;
27
+ }): Promise<{
28
+ finalMessage: M | null;
29
+ exitReason: SessionExitReason;
30
+ usage: ReturnType<AgentStateManager<T>["getTotalUsage"]>;
31
+ }>;
28
32
  }
29
33
 
30
34
  /**
@@ -40,6 +44,7 @@ export interface SessionLifecycleHooks {
40
44
  export const createSession = async <T extends ToolMap, M = unknown>({
41
45
  threadId,
42
46
  agentName,
47
+ description,
43
48
  maxTurns = 50,
44
49
  metadata = {},
45
50
  runAgent,
@@ -86,7 +91,13 @@ export const createSession = async <T extends ToolMap, M = unknown>({
86
91
  };
87
92
 
88
93
  return {
89
- runSession: async ({ stateManager }): Promise<M | null> => {
94
+ runSession: async ({
95
+ stateManager,
96
+ }): Promise<{
97
+ finalMessage: M | null;
98
+ exitReason: SessionExitReason;
99
+ usage: ReturnType<typeof stateManager.getTotalUsage>;
100
+ }> => {
90
101
  setHandler(
91
102
  defineUpdate<unknown, [MessageContent]>(`add${agentName}Message`),
92
103
  async (message: MessageContent) => {
@@ -115,8 +126,6 @@ export const createSession = async <T extends ToolMap, M = unknown>({
115
126
  });
116
127
  }
117
128
 
118
- stateManager.setTools(toolRouter.getToolDefinitions());
119
-
120
129
  await initializeThread(threadId);
121
130
  if (appendSystemPrompt && systemPrompt && systemPrompt.trim() !== "") {
122
131
  await appendSystemMessage(threadId, systemPrompt);
@@ -134,17 +143,29 @@ export const createSession = async <T extends ToolMap, M = unknown>({
134
143
  stateManager.incrementTurns();
135
144
  const currentTurn = stateManager.getTurns();
136
145
 
137
- const { message, rawToolCalls } = await runAgent({
146
+ stateManager.setTools(toolRouter.getToolDefinitions());
147
+
148
+ const { message, rawToolCalls, usage } = await runAgent({
138
149
  threadId,
139
150
  agentName,
140
151
  metadata,
152
+ systemPrompt,
153
+ description,
141
154
  });
142
155
 
156
+ if (usage) {
157
+ stateManager.updateUsage(usage);
158
+ }
159
+
143
160
  // No tools configured - treat any non-end_turn as completed
144
161
  if (!toolRouter.hasTools() || rawToolCalls.length === 0) {
145
162
  stateManager.complete();
146
163
  exitReason = "completed";
147
- return message;
164
+ return {
165
+ finalMessage: message,
166
+ exitReason,
167
+ usage: stateManager.getTotalUsage(),
168
+ };
148
169
  }
149
170
 
150
171
  // Parse all tool calls uniformly through the router
@@ -165,9 +186,18 @@ export const createSession = async <T extends ToolMap, M = unknown>({
165
186
  }
166
187
 
167
188
  // Hooks can call stateManager.waitForInput() to pause the session
168
- await toolRouter.processToolCalls(parsedToolCalls, {
169
- turn: currentTurn,
170
- });
189
+ const toolCallResults = await toolRouter.processToolCalls(
190
+ parsedToolCalls,
191
+ {
192
+ turn: currentTurn,
193
+ }
194
+ );
195
+
196
+ for (const result of toolCallResults) {
197
+ if (result.usage) {
198
+ stateManager.updateUsage(result.usage);
199
+ }
200
+ }
171
201
 
172
202
  if (stateManager.getStatus() === "WAITING_FOR_INPUT") {
173
203
  const conditionMet = await condition(
@@ -195,7 +225,11 @@ export const createSession = async <T extends ToolMap, M = unknown>({
195
225
  await callSessionEnd(exitReason, stateManager.getTurns());
196
226
  }
197
227
 
198
- return null;
228
+ return {
229
+ finalMessage: null,
230
+ exitReason,
231
+ usage: stateManager.getTotalUsage(),
232
+ };
199
233
  },
200
234
  };
201
235
  };
@@ -114,6 +114,25 @@ export interface AgentStateManager<TCustom extends JsonSerializable<TCustom>> {
114
114
 
115
115
  /** Set the tools (converts Zod schemas to JSON Schema for serialization) */
116
116
  setTools(newTools: ToolDefinition[]): void;
117
+
118
+ /** Update the usage */
119
+ updateUsage(usage: {
120
+ inputTokens?: number;
121
+ outputTokens?: number;
122
+ cachedWriteTokens?: number;
123
+ cachedReadTokens?: number;
124
+ reasonTokens?: number;
125
+ }): void;
126
+
127
+ /** Get the total usage */
128
+ getTotalUsage(): {
129
+ totalInputTokens: number;
130
+ totalOutputTokens: number;
131
+ totalCachedWriteTokens: number;
132
+ totalCachedReadTokens: number;
133
+ totalReasonTokens: number;
134
+ turns: number;
135
+ };
117
136
  }
118
137
 
119
138
  /**
@@ -140,6 +159,11 @@ export function createAgentStateManager<
140
159
  let version = initialState?.version ?? 0;
141
160
  let turns = initialState?.turns ?? 0;
142
161
  let tools = initialState?.tools ?? [];
162
+ let totalInputTokens = 0;
163
+ let totalOutputTokens = 0;
164
+ let totalCachedWriteTokens = 0;
165
+ let totalCachedReadTokens = 0;
166
+ let totalReasonTokens = 0;
143
167
 
144
168
  // Tasks state
145
169
  const tasks = new Map<string, WorkflowTask>(initialState?.tasks);
@@ -283,6 +307,38 @@ export function createAgentStateManager<
283
307
  }
284
308
  return deleted;
285
309
  },
310
+
311
+ updateUsage(usage: {
312
+ inputTokens?: number;
313
+ outputTokens?: number;
314
+ cachedWriteTokens?: number;
315
+ cachedReadTokens?: number;
316
+ reasonTokens?: number;
317
+ }): void {
318
+ totalInputTokens += usage.inputTokens ?? 0;
319
+ totalOutputTokens += usage.outputTokens ?? 0;
320
+ totalCachedWriteTokens += usage.cachedWriteTokens ?? 0;
321
+ totalCachedReadTokens += usage.cachedReadTokens ?? 0;
322
+ totalReasonTokens += usage.reasonTokens ?? 0;
323
+ },
324
+
325
+ getTotalUsage(): {
326
+ totalInputTokens: number;
327
+ totalOutputTokens: number;
328
+ totalCachedWriteTokens: number;
329
+ totalCachedReadTokens: number;
330
+ totalReasonTokens: number;
331
+ turns: number;
332
+ } {
333
+ return {
334
+ totalInputTokens,
335
+ totalOutputTokens,
336
+ totalCachedWriteTokens,
337
+ totalCachedReadTokens,
338
+ totalReasonTokens,
339
+ turns,
340
+ };
341
+ },
286
342
  };
287
343
  }
288
344
 
@@ -6,13 +6,14 @@ import type {
6
6
  PreToolUseHookResult,
7
7
  SubagentConfig,
8
8
  SubagentHooks,
9
+ TokenUsage,
9
10
  ToolHooks,
10
11
  ToolResultConfig,
11
12
  } from "./types";
12
13
  import type { SubagentArgs } from "../tools/subagent/tool";
13
14
 
14
15
  import type { z } from "zod";
15
- import { createSubagentTool } from "../tools/subagent/tool";
16
+ import { createSubagentTool, SUBAGENT_TOOL_NAME } from "../tools/subagent/tool";
16
17
  import { createSubagentHandler } from "../tools/subagent/handler";
17
18
 
18
19
  export type { ToolMessageContent };
@@ -53,7 +54,7 @@ export interface ToolWithHandler<
53
54
  strict?: boolean;
54
55
  max_uses?: number;
55
56
  /** Whether this tool is available to the agent (default: true). Disabled tools are excluded from definitions and rejected at parse time. */
56
- enabled?: boolean;
57
+ enabled?: () => boolean;
57
58
  /** Per-tool lifecycle hooks (run in addition to global hooks) */
58
59
  hooks?: ToolHooks<z.infer<TSchema>, TResult>;
59
60
  }
@@ -76,7 +77,7 @@ export type ToolMap = Record<
76
77
  handler: ToolHandler<any, any, any>;
77
78
  strict?: boolean;
78
79
  max_uses?: number;
79
- enabled?: boolean;
80
+ enabled?: () => boolean;
80
81
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
82
  hooks?: ToolHooks<any, any>;
82
83
  }
@@ -153,6 +154,8 @@ export interface ToolHandlerResponse<TResult = null> {
153
154
  * payloads through Temporal's activity payload limit.
154
155
  */
155
156
  resultAppended?: boolean;
157
+ /** Token usage from the tool execution (e.g. child agent invocations) */
158
+ usage?: TokenUsage;
156
159
  }
157
160
 
158
161
  /**
@@ -228,6 +231,7 @@ export interface ToolCallResult<
228
231
  toolCallId: string;
229
232
  name: TName;
230
233
  data: TResult;
234
+ usage?: TokenUsage;
231
235
  }
232
236
 
233
237
  /**
@@ -412,25 +416,23 @@ export function createToolRouter<T extends ToolMap>(
412
416
  }
413
417
 
414
418
  /** Check if a tool is enabled (defaults to true when not specified) */
415
- const isEnabled = (tool: ToolMap[string]): boolean => tool.enabled !== false;
419
+ const isEnabled = (tool: ToolMap[string] | SubagentConfig): boolean =>
420
+ tool.enabled?.() ?? true;
416
421
 
417
422
  if (options.subagents) {
418
- const enabledSubagents = options.subagents.filter(
419
- (s) => s.enabled !== false
420
- );
421
- if (enabledSubagents.length > 0) {
423
+ if (options.subagents.length > 0) {
422
424
  // Build per-subagent hook dispatcher keyed by subagent name
423
425
  const subagentHooksMap = new Map<string, SubagentHooks>();
424
- for (const s of enabledSubagents) {
426
+ for (const s of options.subagents) {
425
427
  if (s.hooks) subagentHooksMap.set(s.agentName, s.hooks);
426
428
  }
427
429
 
428
430
  const resolveSubagentName = (args: unknown): string =>
429
431
  (args as SubagentArgs).subagent;
430
432
 
431
- toolMap.set("Subagent", {
432
- ...createSubagentTool(enabledSubagents),
433
- handler: createSubagentHandler(enabledSubagents),
433
+ toolMap.set(SUBAGENT_TOOL_NAME, {
434
+ ...createSubagentTool(options.subagents),
435
+ handler: createSubagentHandler(options.subagents),
434
436
  ...(subagentHooksMap.size > 0 && {
435
437
  hooks: {
436
438
  onPreToolUse: async (ctx): Promise<PreToolUseHookResult> => {
@@ -655,15 +657,25 @@ export function createToolRouter<T extends ToolMap>(
655
657
  },
656
658
 
657
659
  getToolDefinitions(): ToolDefinition[] {
658
- return Array.from(toolMap)
659
- .filter(([, tool]) => isEnabled(tool))
660
- .map(([name, tool]) => ({
661
- name,
662
- description: tool.description,
663
- schema: tool.schema,
664
- strict: tool.strict,
665
- max_uses: tool.max_uses,
666
- }));
660
+ const activeSubagents =
661
+ options.subagents?.filter((subagent) => isEnabled(subagent)) ?? [];
662
+
663
+ return [
664
+ ...Array.from(toolMap)
665
+ .filter(
666
+ ([, tool]) => isEnabled(tool) && tool.name !== SUBAGENT_TOOL_NAME
667
+ )
668
+ .map(([name, tool]) => ({
669
+ name,
670
+ description: tool.description,
671
+ schema: tool.schema,
672
+ strict: tool.strict,
673
+ max_uses: tool.max_uses,
674
+ })),
675
+ ...(activeSubagents.length > 0
676
+ ? [createSubagentTool(activeSubagents)]
677
+ : []),
678
+ ];
667
679
  },
668
680
 
669
681
  // --- Methods for processing tool calls ---
package/src/lib/types.ts CHANGED
@@ -32,6 +32,10 @@ export interface BaseAgentState {
32
32
  turns: number;
33
33
  tasks: Map<string, WorkflowTask>;
34
34
  systemPrompt: string;
35
+ totalInputTokens: number;
36
+ totalOutputTokens: number;
37
+ cachedWriteTokens: number;
38
+ cachedReadtTokens: number;
35
39
  }
36
40
 
37
41
  /**
@@ -50,17 +54,21 @@ export interface AgentFile {
50
54
  mimeType?: string;
51
55
  }
52
56
 
57
+ export interface TokenUsage {
58
+ inputTokens?: number;
59
+ outputTokens?: number;
60
+ cachedWriteTokens?: number;
61
+ cachedReadTokens?: number;
62
+ reasonTokens?: number;
63
+ }
64
+
53
65
  /**
54
66
  * Agent response from LLM invocation
55
67
  */
56
68
  export interface AgentResponse<M = StoredMessage> {
57
69
  message: M;
58
70
  rawToolCalls: RawToolCall[];
59
- usage?: {
60
- input_tokens?: number;
61
- output_tokens?: number;
62
- total_tokens?: number;
63
- };
71
+ usage?: TokenUsage;
64
72
  }
65
73
 
66
74
  /**
@@ -143,9 +151,10 @@ export interface SerializableToolDefinition {
143
151
  /**
144
152
  * Configuration passed to runAgent activity
145
153
  */
146
- export interface RunAgentConfig {
154
+ export interface RunAgentConfig extends AgentConfig {
155
+ /** The thread ID to use for the session */
147
156
  threadId: string;
148
- agentName: string;
157
+ /** Metadata for the session */
149
158
  metadata?: Record<string, unknown>;
150
159
  }
151
160
 
@@ -155,6 +164,7 @@ export interface RunAgentConfig {
155
164
  export type RunAgentActivity<M = StoredMessage> = (
156
165
  config: RunAgentConfig
157
166
  ) => Promise<AgentResponse<M>>;
167
+
158
168
  /**
159
169
  * Configuration for appending a tool result
160
170
  */
@@ -186,7 +196,7 @@ export interface SubagentConfig<TResult extends z.ZodType = z.ZodType> {
186
196
  /** Description shown to the parent agent explaining what this subagent does */
187
197
  description: string;
188
198
  /** Whether this subagent is available (default: true). Disabled subagents are excluded from the Subagent tool. */
189
- enabled?: boolean;
199
+ enabled?: () => boolean;
190
200
  /** Temporal workflow function or type name (used with executeChild) */
191
201
  workflow: string | Workflow;
192
202
  /** Optional task queue - defaults to parent's queue if not specified */
@@ -1,11 +1,11 @@
1
- import type { ToolHandler } from "../../lib/tool-router";
1
+ import type { ActivityToolHandler } from "../../lib/tool-router";
2
2
  import type { AskUserQuestionArgs } from "./tool";
3
3
 
4
4
  /**
5
5
  * Creates handler for user interaction tool - creates AI messages for display.
6
6
  */
7
7
  export const createAskUserQuestionHandler =
8
- (): ToolHandler<
8
+ (): ActivityToolHandler<
9
9
  AskUserQuestionArgs,
10
10
  {
11
11
  questions: {
@@ -16,7 +16,7 @@ export const createAskUserQuestionHandler =
16
16
  }[];
17
17
  }
18
18
  > =>
19
- (args) => {
19
+ async (args) => {
20
20
  return {
21
21
  toolResponse: "Question submitted",
22
22
  data: { questions: args.questions },
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import type { ToolDefinition } from "../../lib/tool-router";
3
3
 
4
- export const readTool = {
4
+ export const readFileTool = {
5
5
  name: "FileRead" as const,
6
6
  description: `Read file contents with optional pagination.
7
7
 
@@ -31,4 +31,4 @@ The tool returns the file content in an appropriate format:
31
31
  strict: true,
32
32
  } satisfies ToolDefinition;
33
33
 
34
- export type FileReadArgs = z.infer<typeof readTool.schema>;
34
+ export type FileReadArgs = z.infer<typeof readFileTool.schema>;
@@ -53,7 +53,7 @@ export function createSubagentHandler<
53
53
  taskQueue: config.taskQueue ?? parentTaskQueue,
54
54
  };
55
55
 
56
- const { toolResponse, data } =
56
+ const { toolResponse, data, usage } =
57
57
  typeof config.workflow === "string"
58
58
  ? await executeChild(config.workflow, childOpts)
59
59
  : await executeChild(config.workflow, childOpts);
@@ -66,6 +66,7 @@ export function createSubagentHandler<
66
66
  return {
67
67
  toolResponse,
68
68
  data: validated,
69
+ ...(usage && { usage }),
69
70
  };
70
71
  };
71
72
  }
@@ -1,7 +1,7 @@
1
1
  import z from "zod";
2
2
  import type { SubagentConfig } from "../../lib/types";
3
3
 
4
- const SUBAGENT_TOOL = "Subagent" as const;
4
+ export const SUBAGENT_TOOL_NAME = "Subagent" as const;
5
5
 
6
6
  /**
7
7
  * Builds the tool description with available subagent information
@@ -13,13 +13,13 @@ function buildSubagentDescription(subagents: SubagentConfig[]): string {
13
13
 
14
14
  return `Launch a new agent to handle complex tasks autonomously.
15
15
 
16
- The ${SUBAGENT_TOOL} tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.
16
+ The ${SUBAGENT_TOOL_NAME} tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.
17
17
 
18
18
  Available agent types:
19
19
 
20
20
  ${subagentList}
21
21
 
22
- When using the ${SUBAGENT_TOOL} tool, you must specify a subagent parameter to select which agent type to use.
22
+ When using the ${SUBAGENT_TOOL_NAME} tool, you must specify a subagent parameter to select which agent type to use.
23
23
 
24
24
  Usage notes:
25
25
 
@@ -67,7 +67,7 @@ export function createSubagentTool<T extends SubagentConfig[]>(
67
67
  const names = subagents.map((s) => s.agentName);
68
68
 
69
69
  return {
70
- name: SUBAGENT_TOOL,
70
+ name: SUBAGENT_TOOL_NAME,
71
71
  description: buildSubagentDescription(subagents),
72
72
  schema: z.object({
73
73
  subagent: z.enum(names).describe("The type of subagent to launch"),
@@ -1,25 +1,24 @@
1
1
  import { z } from "zod";
2
2
  import type { ToolDefinition } from "../../lib/tool-router";
3
3
 
4
- export const writeTool = {
4
+ export const writeFileTool = {
5
5
  name: "FileWrite" as const,
6
6
  description: `Create or overwrite a file with new content.
7
7
 
8
8
  Usage:
9
- - Provide the absolute path to the file
10
9
  - The file will be created if it doesn't exist
11
10
  - If the file exists, it will be completely overwritten
12
11
 
13
12
  IMPORTANT:
14
13
  - You must read the file first (in this session) before writing to it
15
14
  - This is an atomic write operation - the entire file is replaced
16
- - Path must be absolute (e.g., "/docs/readme.md", not "docs/readme.md")
15
+ - Path must be relative to the root of the file system (e.g., "docs/readme.md", not "/docs/readme.md")
17
16
  `,
18
17
  schema: z.object({
19
- file_path: z.string().describe("The absolute path to the file to write"),
18
+ file_path: z.string().describe("The path to the file to write"),
20
19
  content: z.string().describe("The content to write to the file"),
21
20
  }),
22
21
  strict: true,
23
22
  } satisfies ToolDefinition;
24
23
 
25
- export type FileWriteArgs = z.infer<typeof writeTool.schema>;
24
+ export type FileWriteArgs = z.infer<typeof writeFileTool.schema>;
package/src/workflow.ts CHANGED
@@ -113,9 +113,9 @@ export { globTool } from "./tools/glob/tool";
113
113
  export type { GlobArgs } from "./tools/glob/tool";
114
114
  export { grepTool } from "./tools/grep/tool";
115
115
  export type { GrepArgs } from "./tools/grep/tool";
116
- export { readTool } from "./tools/read-file/tool";
116
+ export { readFileTool } from "./tools/read-file/tool";
117
117
  export type { FileReadArgs } from "./tools/read-file/tool";
118
- export { writeTool } from "./tools/write-file/tool";
118
+ export { writeFileTool } from "./tools/write-file/tool";
119
119
  export type { FileWriteArgs } from "./tools/write-file/tool";
120
120
  export { editTool } from "./tools/edit/tool";
121
121
  export type { FileEditArgs } from "./tools/edit/tool";