@dotsetlabs/dotclaw 1.2.0 → 1.4.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.
Files changed (77) hide show
  1. package/README.md +9 -0
  2. package/config-examples/runtime.json +119 -2
  3. package/container/agent-runner/src/agent-config.ts +20 -2
  4. package/container/agent-runner/src/container-protocol.ts +11 -0
  5. package/container/agent-runner/src/index.ts +39 -7
  6. package/container/agent-runner/src/tools.ts +84 -5
  7. package/dist/agent-context.d.ts +5 -0
  8. package/dist/agent-context.d.ts.map +1 -1
  9. package/dist/agent-context.js +19 -8
  10. package/dist/agent-context.js.map +1 -1
  11. package/dist/agent-execution.d.ts +6 -0
  12. package/dist/agent-execution.d.ts.map +1 -1
  13. package/dist/agent-execution.js +61 -4
  14. package/dist/agent-execution.js.map +1 -1
  15. package/dist/background-job-classifier.d.ts +20 -0
  16. package/dist/background-job-classifier.d.ts.map +1 -0
  17. package/dist/background-job-classifier.js +145 -0
  18. package/dist/background-job-classifier.js.map +1 -0
  19. package/dist/background-jobs.d.ts.map +1 -1
  20. package/dist/background-jobs.js +81 -4
  21. package/dist/background-jobs.js.map +1 -1
  22. package/dist/cli.js +343 -11
  23. package/dist/cli.js.map +1 -1
  24. package/dist/config.d.ts +0 -2
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/config.js +0 -3
  27. package/dist/config.js.map +1 -1
  28. package/dist/container-protocol.d.ts +11 -0
  29. package/dist/container-protocol.d.ts.map +1 -1
  30. package/dist/container-runner.d.ts.map +1 -1
  31. package/dist/container-runner.js +9 -1
  32. package/dist/container-runner.js.map +1 -1
  33. package/dist/dashboard.d.ts +5 -0
  34. package/dist/dashboard.d.ts.map +1 -1
  35. package/dist/dashboard.js +58 -8
  36. package/dist/dashboard.js.map +1 -1
  37. package/dist/db.d.ts +16 -0
  38. package/dist/db.d.ts.map +1 -1
  39. package/dist/db.js +53 -0
  40. package/dist/db.js.map +1 -1
  41. package/dist/index.js +403 -30
  42. package/dist/index.js.map +1 -1
  43. package/dist/json-helpers.d.ts +6 -0
  44. package/dist/json-helpers.d.ts.map +1 -0
  45. package/dist/json-helpers.js +17 -0
  46. package/dist/json-helpers.js.map +1 -0
  47. package/dist/logger.d.ts +0 -1
  48. package/dist/logger.d.ts.map +1 -1
  49. package/dist/logger.js +1 -1
  50. package/dist/logger.js.map +1 -1
  51. package/dist/metrics.d.ts +3 -0
  52. package/dist/metrics.d.ts.map +1 -1
  53. package/dist/metrics.js +35 -1
  54. package/dist/metrics.js.map +1 -1
  55. package/dist/planner-probe.d.ts +14 -0
  56. package/dist/planner-probe.d.ts.map +1 -0
  57. package/dist/planner-probe.js +97 -0
  58. package/dist/planner-probe.js.map +1 -0
  59. package/dist/progress.d.ts +27 -0
  60. package/dist/progress.d.ts.map +1 -1
  61. package/dist/progress.js +151 -0
  62. package/dist/progress.js.map +1 -1
  63. package/dist/request-router.d.ts +34 -0
  64. package/dist/request-router.d.ts.map +1 -0
  65. package/dist/request-router.js +148 -0
  66. package/dist/request-router.js.map +1 -0
  67. package/dist/runtime-config.d.ts +81 -0
  68. package/dist/runtime-config.d.ts.map +1 -1
  69. package/dist/runtime-config.js +190 -13
  70. package/dist/runtime-config.js.map +1 -1
  71. package/dist/task-scheduler.d.ts.map +1 -1
  72. package/dist/task-scheduler.js +56 -9
  73. package/dist/task-scheduler.js.map +1 -1
  74. package/dist/trace-writer.d.ts +1 -0
  75. package/dist/trace-writer.d.ts.map +1 -1
  76. package/dist/trace-writer.js.map +1 -1
  77. package/package.json +1 -1
package/README.md CHANGED
@@ -45,6 +45,8 @@ After installation, use the `dotclaw` CLI:
45
45
  ```bash
46
46
  dotclaw setup # Full setup (init + configure + build + install service)
47
47
  dotclaw configure # Re-configure API keys and model
48
+ dotclaw add-instance # Create and start an isolated instance
49
+ dotclaw instances # List discovered instances
48
50
  dotclaw build # Build the Docker container image
49
51
  dotclaw start # Start the service
50
52
  dotclaw stop # Stop the service
@@ -55,6 +57,13 @@ dotclaw doctor # Run diagnostics
55
57
  dotclaw register # Register a new Telegram chat
56
58
  ```
57
59
 
60
+ Instance flags:
61
+
62
+ ```bash
63
+ dotclaw status --id dev # Run against a specific instance (~/.dotclaw-dev)
64
+ dotclaw restart --all # Restart all instances
65
+ ```
66
+
58
67
  ## Configuration
59
68
 
60
69
  All configuration and data is stored in `~/.dotclaw/`:
@@ -2,7 +2,8 @@
2
2
  "host": {
3
3
  "logLevel": "info",
4
4
  "container": {
5
- "mode": "daemon"
5
+ "mode": "daemon",
6
+ "instanceId": ""
6
7
  },
7
8
  "metrics": {
8
9
  "port": 3001,
@@ -19,7 +20,109 @@
19
20
  "backgroundJobs": {
20
21
  "enabled": true,
21
22
  "maxConcurrent": 2,
22
- "maxRuntimeMs": 2400000
23
+ "maxRuntimeMs": 2400000,
24
+ "progress": {
25
+ "enabled": true,
26
+ "startDelayMs": 30000,
27
+ "intervalMs": 120000,
28
+ "maxUpdates": 3
29
+ },
30
+ "autoSpawn": {
31
+ "enabled": true,
32
+ "foregroundTimeoutMs": 90000,
33
+ "onTimeout": true,
34
+ "onToolLimit": true,
35
+ "classifier": {
36
+ "enabled": true,
37
+ "model": "openai/gpt-5-nano",
38
+ "timeoutMs": 3000,
39
+ "maxOutputTokens": 32,
40
+ "temperature": 0,
41
+ "confidenceThreshold": 0.6,
42
+ "adaptive": {
43
+ "enabled": true,
44
+ "minThreshold": 0.55,
45
+ "maxThreshold": 0.65,
46
+ "queueDepthLow": 0,
47
+ "queueDepthHigh": 4
48
+ }
49
+ }
50
+ }
51
+ },
52
+ "routing": {
53
+ "enabled": true,
54
+ "maxFastChars": 200,
55
+ "maxStandardChars": 1200,
56
+ "backgroundMinChars": 2000,
57
+ "fastKeywords": ["hi", "hello", "hey", "who are you", "what can you do"],
58
+ "deepKeywords": ["research", "analysis", "report", "dashboard", "refactor", "architecture", "design"],
59
+ "backgroundKeywords": ["background", "long-running", "research", "dashboard", "refactor", "report"],
60
+ "classifierFallback": {
61
+ "enabled": true,
62
+ "minChars": 600
63
+ },
64
+ "plannerProbe": {
65
+ "enabled": true,
66
+ "model": "openai/gpt-5-nano",
67
+ "timeoutMs": 3000,
68
+ "maxOutputTokens": 120,
69
+ "temperature": 0,
70
+ "minChars": 700,
71
+ "minSteps": 4,
72
+ "minTools": 3
73
+ },
74
+ "profiles": {
75
+ "fast": {
76
+ "model": "openai/gpt-5-nano",
77
+ "maxOutputTokens": 256,
78
+ "maxToolSteps": 6,
79
+ "recallMaxResults": 0,
80
+ "recallMaxTokens": 0,
81
+ "enablePlanner": false,
82
+ "enableValidation": false,
83
+ "responseValidationMaxRetries": 0,
84
+ "enableMemoryRecall": false,
85
+ "enableMemoryExtraction": false,
86
+ "progress": { "enabled": false }
87
+ },
88
+ "standard": {
89
+ "model": "openai/gpt-5-mini",
90
+ "maxOutputTokens": 768,
91
+ "maxToolSteps": 16,
92
+ "recallMaxResults": 6,
93
+ "recallMaxTokens": 1500,
94
+ "enablePlanner": true,
95
+ "enableValidation": true,
96
+ "responseValidationMaxRetries": 1,
97
+ "enableMemoryRecall": true,
98
+ "enableMemoryExtraction": true
99
+ },
100
+ "deep": {
101
+ "model": "moonshotai/kimi-k2.5",
102
+ "maxOutputTokens": 1536,
103
+ "maxToolSteps": 32,
104
+ "recallMaxResults": 12,
105
+ "recallMaxTokens": 2500,
106
+ "enablePlanner": true,
107
+ "enableValidation": true,
108
+ "responseValidationMaxRetries": 2,
109
+ "enableMemoryRecall": true,
110
+ "enableMemoryExtraction": true
111
+ },
112
+ "background": {
113
+ "model": "moonshotai/kimi-k2.5",
114
+ "maxOutputTokens": 2048,
115
+ "maxToolSteps": 64,
116
+ "recallMaxResults": 16,
117
+ "recallMaxTokens": 4000,
118
+ "enablePlanner": true,
119
+ "enableValidation": true,
120
+ "responseValidationMaxRetries": 2,
121
+ "enableMemoryRecall": true,
122
+ "enableMemoryExtraction": true,
123
+ "progress": { "enabled": true, "initialMs": 15000, "intervalMs": 60000, "maxUpdates": 3 }
124
+ }
125
+ }
23
126
  }
24
127
  },
25
128
  "agent": {
@@ -30,6 +133,20 @@
30
133
  "planner": {
31
134
  "enabled": true,
32
135
  "mode": "auto"
136
+ },
137
+ "responseValidation": {
138
+ "enabled": true,
139
+ "minPromptTokens": 400,
140
+ "minResponseTokens": 160
141
+ },
142
+ "tools": {
143
+ "progress": {
144
+ "enabled": true,
145
+ "minIntervalMs": 30000,
146
+ "notifyTools": ["WebSearch", "WebFetch", "Bash", "GitClone", "NpmInstall"],
147
+ "notifyOnStart": true,
148
+ "notifyOnError": true
149
+ }
33
150
  }
34
151
  }
35
152
  }
@@ -60,6 +60,8 @@ export type AgentRuntimeConfig = {
60
60
  temperature: number;
61
61
  maxRetries: number;
62
62
  allowToolCalls: boolean;
63
+ minPromptTokens: number;
64
+ minResponseTokens: number;
63
65
  };
64
66
  tools: {
65
67
  maxToolSteps: number;
@@ -87,6 +89,13 @@ export type AgentRuntimeConfig = {
87
89
  maxBytes: number;
88
90
  httpTimeoutMs: number;
89
91
  };
92
+ progress: {
93
+ enabled: boolean;
94
+ minIntervalMs: number;
95
+ notifyTools: string[];
96
+ notifyOnStart: boolean;
97
+ notifyOnError: boolean;
98
+ };
90
99
  toolSummary: {
91
100
  enabled: boolean;
92
101
  maxBytes: number;
@@ -160,7 +169,7 @@ const DEFAULT_AGENT_CONFIG: AgentRuntimeConfig['agent'] = {
160
169
  planner: {
161
170
  enabled: true,
162
171
  mode: 'auto',
163
- minTokens: 600,
172
+ minTokens: 800,
164
173
  triggerRegex: '(plan|steps|roadmap|research|design|architecture|spec|strategy)',
165
174
  maxOutputTokens: 200,
166
175
  temperature: 0.2
@@ -170,7 +179,9 @@ const DEFAULT_AGENT_CONFIG: AgentRuntimeConfig['agent'] = {
170
179
  maxOutputTokens: 120,
171
180
  temperature: 0,
172
181
  maxRetries: 1,
173
- allowToolCalls: false
182
+ allowToolCalls: false,
183
+ minPromptTokens: 400,
184
+ minResponseTokens: 160
174
185
  },
175
186
  tools: {
176
187
  maxToolSteps: 24,
@@ -198,6 +209,13 @@ const DEFAULT_AGENT_CONFIG: AgentRuntimeConfig['agent'] = {
198
209
  maxBytes: 800_000,
199
210
  httpTimeoutMs: 20_000
200
211
  },
212
+ progress: {
213
+ enabled: true,
214
+ minIntervalMs: 30_000,
215
+ notifyTools: ['WebSearch', 'WebFetch', 'Bash', 'GitClone', 'NpmInstall'],
216
+ notifyOnStart: true,
217
+ notifyOnError: true
218
+ },
201
219
  toolSummary: {
202
220
  enabled: true,
203
221
  maxBytes: 60_000,
@@ -40,6 +40,11 @@ export interface ContainerInput {
40
40
  modelContextTokens?: number;
41
41
  modelMaxOutputTokens?: number;
42
42
  modelTemperature?: number;
43
+ timezone?: string;
44
+ disablePlanner?: boolean;
45
+ disableResponseValidation?: boolean;
46
+ responseValidationMaxRetries?: number;
47
+ disableMemoryExtraction?: boolean;
43
48
  streaming?: {
44
49
  enabled?: boolean;
45
50
  draftId?: number;
@@ -63,6 +68,12 @@ export interface ContainerOutput {
63
68
  session_recall_count?: number;
64
69
  memory_items_upserted?: number;
65
70
  memory_items_extracted?: number;
71
+ timings?: {
72
+ planner_ms?: number;
73
+ response_validation_ms?: number;
74
+ memory_extraction_ms?: number;
75
+ tool_ms?: number;
76
+ };
66
77
  tool_calls?: Array<{
67
78
  name: string;
68
79
  args?: unknown;
@@ -318,6 +318,7 @@ function buildSystemInstructions(params: {
318
318
  taskId?: string;
319
319
  isBackgroundJob: boolean;
320
320
  jobId?: string;
321
+ timezone?: string;
321
322
  planBlock?: string;
322
323
  taskExtractionPack?: PromptPack | null;
323
324
  responseQualityPack?: PromptPack | null;
@@ -342,7 +343,7 @@ function buildSystemInstructions(params: {
342
343
  '- `mcp__dotclaw__spawn_job`: start a background job.',
343
344
  '- `mcp__dotclaw__job_status`, `mcp__dotclaw__list_jobs`, `mcp__dotclaw__cancel_job`.',
344
345
  '- `mcp__dotclaw__job_update`: log job progress or notify the user.',
345
- 'Tip: For long-running work (multi-step coding, large research), prefer `mcp__dotclaw__spawn_job` and report back when done.',
346
+ 'Rule: If the task is likely to take more than ~2 minutes or needs multi-step research/coding, you MUST call `mcp__dotclaw__spawn_job` immediately and tell the user you queued it. Do not run long tasks in the foreground.',
346
347
  '- `mcp__dotclaw__register_group`: main group only.',
347
348
  '- `mcp__dotclaw__remove_group`, `mcp__dotclaw__list_groups`: main group only.',
348
349
  '- `mcp__dotclaw__set_model`: main group only.',
@@ -425,6 +426,10 @@ function buildSystemInstructions(params: {
425
426
  ? `Behavior overrides:\n${JSON.stringify(params.behaviorConfig, null, 2)}`
426
427
  : '';
427
428
 
429
+ const timezoneNote = params.timezone
430
+ ? `Timezone: ${params.timezone}. Use this timezone when interpreting or presenting timestamps unless the user specifies another.`
431
+ : '';
432
+
428
433
  const scheduledNote = params.isScheduledTask
429
434
  ? `You are running as a scheduled task${params.taskId ? ` (task id: ${params.taskId})` : ''}. If you need to communicate, use \`mcp__dotclaw__send_message\`.`
430
435
  : '';
@@ -432,7 +437,7 @@ function buildSystemInstructions(params: {
432
437
  ? 'You are running in the background for a user request. Focus on completing the task and return a complete response without asking follow-up questions unless strictly necessary.'
433
438
  : '';
434
439
  const jobNote = params.isBackgroundJob
435
- ? `You are running as a background job${params.jobId ? ` (job id: ${params.jobId})` : ''}. Return a complete result. Use \`mcp__dotclaw__job_update\` for progress if needed. Prefer writing large outputs to the job artifacts directory.`
440
+ ? `You are running as a background job${params.jobId ? ` (job id: ${params.jobId})` : ''}. Return a complete result. If the task will take more than a few minutes, send periodic \`mcp__dotclaw__job_update\` messages with milestones or intermediate findings (roughly every ~5 minutes or after major steps). Prefer writing large outputs to the job artifacts directory.`
436
441
  : '';
437
442
  const jobArtifactsNote = params.isBackgroundJob && params.jobId
438
443
  ? `Job artifacts directory: /workspace/group/jobs/${params.jobId}`
@@ -496,6 +501,7 @@ function buildSystemInstructions(params: {
496
501
  browserAutomation,
497
502
  groupNotes,
498
503
  globalNotes,
504
+ timezoneNote,
499
505
  params.planBlock || '',
500
506
  toolCallingBlock,
501
507
  toolOutcomeBlock,
@@ -835,26 +841,30 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
835
841
  const maxToolSteps = Number.isFinite(input.maxToolSteps)
836
842
  ? Math.max(1, Math.floor(input.maxToolSteps as number))
837
843
  : agent.tools.maxToolSteps;
838
- const memoryExtractionEnabled = agent.memory.extraction.enabled;
844
+ const memoryExtractionEnabled = agent.memory.extraction.enabled && !input.disableMemoryExtraction;
839
845
  const isDaemon = process.env.DOTCLAW_DAEMON === '1';
840
846
  const memoryExtractionAsync = agent.memory.extraction.async;
841
847
  const memoryExtractionMaxMessages = agent.memory.extraction.maxMessages;
842
848
  const memoryExtractionMaxOutputTokens = agent.memory.extraction.maxOutputTokens;
843
849
  const memoryExtractScheduled = agent.memory.extractScheduled;
844
850
  const memoryArchiveSync = agent.memory.archiveSync;
845
- const plannerEnabled = agent.planner.enabled;
851
+ const plannerEnabled = agent.planner.enabled && !input.disablePlanner;
846
852
  const plannerMode = String(agent.planner.mode || 'auto').toLowerCase();
847
853
  const plannerMinTokens = agent.planner.minTokens;
848
854
  const plannerTrigger = buildPlannerTrigger(agent.planner.triggerRegex);
849
855
  const plannerModel = agent.models.planner;
850
856
  const plannerMaxOutputTokens = agent.planner.maxOutputTokens;
851
857
  const plannerTemperature = agent.planner.temperature;
852
- const responseValidateEnabled = agent.responseValidation.enabled;
858
+ const responseValidateEnabled = agent.responseValidation.enabled && !input.disableResponseValidation;
853
859
  const responseValidateModel = agent.models.responseValidation;
854
860
  const responseValidateMaxOutputTokens = agent.responseValidation.maxOutputTokens;
855
861
  const responseValidateTemperature = agent.responseValidation.temperature;
856
- const responseValidateMaxRetries = agent.responseValidation.maxRetries;
862
+ const responseValidateMaxRetries = Number.isFinite(input.responseValidationMaxRetries)
863
+ ? Math.max(0, Math.floor(input.responseValidationMaxRetries as number))
864
+ : agent.responseValidation.maxRetries;
857
865
  const responseValidateAllowToolCalls = agent.responseValidation.allowToolCalls;
866
+ const responseValidateMinPromptTokens = agent.responseValidation.minPromptTokens || 0;
867
+ const responseValidateMinResponseTokens = agent.responseValidation.minResponseTokens || 0;
858
868
  const maxContextMessageTokens = agent.context.maxContextMessageTokens;
859
869
  const streamingEnabled = Boolean(input.streaming?.enabled && typeof input.streaming?.draftId === 'number');
860
870
  const streamingDraftId = streamingEnabled ? input.streaming?.draftId : undefined;
@@ -884,6 +894,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
884
894
  const toolCalls: ToolCallRecord[] = [];
885
895
  let memoryItemsUpserted = 0;
886
896
  let memoryItemsExtracted = 0;
897
+ const timings: { planner_ms?: number; response_validation_ms?: number; memory_extraction_ms?: number; tool_ms?: number } = {};
887
898
  const ipc = createIpcHandlers({
888
899
  chatJid: input.chatJid,
889
900
  groupFolder: input.groupFolder,
@@ -897,7 +908,11 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
897
908
  onToolCall: (call) => {
898
909
  toolCalls.push(call);
899
910
  },
900
- policy: input.toolPolicy
911
+ policy: input.toolPolicy,
912
+ jobProgress: {
913
+ jobId: input.jobId,
914
+ enabled: Boolean(input.isBackgroundJob)
915
+ }
901
916
  });
902
917
 
903
918
  let streamLastSentAt = 0;
@@ -1083,6 +1098,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1083
1098
  taskId: input.taskId,
1084
1099
  isBackgroundJob: !!input.isBackgroundJob,
1085
1100
  jobId: input.jobId,
1101
+ timezone: typeof input.timezone === 'string' ? input.timezone : undefined,
1086
1102
  planBlock: planBlockValue,
1087
1103
  taskExtractionPack: taskPackResult?.pack || null,
1088
1104
  responseQualityPack: responseQualityResult?.pack || null,
@@ -1109,6 +1125,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1109
1125
  trigger: plannerTrigger
1110
1126
  })) {
1111
1127
  try {
1128
+ const plannerStartedAt = Date.now();
1112
1129
  const plannerPrompt = buildPlannerPrompt(plannerContextMessages);
1113
1130
  const plannerResult = await openrouter.callModel({
1114
1131
  model: plannerModel,
@@ -1122,6 +1139,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1122
1139
  if (plan) {
1123
1140
  planBlock = formatPlanBlock(plan);
1124
1141
  }
1142
+ timings.planner_ms = Date.now() - plannerStartedAt;
1125
1143
  } catch (err) {
1126
1144
  log(`Planner failed: ${err instanceof Error ? err.message : String(err)}`);
1127
1145
  }
@@ -1245,6 +1263,8 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1245
1263
  modelToolCalls = firstAttempt.modelToolCalls;
1246
1264
 
1247
1265
  const shouldValidate = responseValidateEnabled
1266
+ && promptTokens >= responseValidateMinPromptTokens
1267
+ && completionTokens >= responseValidateMinResponseTokens
1248
1268
  && (responseValidateAllowToolCalls || modelToolCalls.length === 0);
1249
1269
  if (shouldValidate) {
1250
1270
  let retriesLeft = responseValidateMaxRetries;
@@ -1257,6 +1277,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1257
1277
  validationResult = { verdict: 'fail', issues: ['Response was empty.'], missing: [] };
1258
1278
  } else {
1259
1279
  try {
1280
+ const validationStartedAt = Date.now();
1260
1281
  validationResult = await validateResponseQuality({
1261
1282
  openrouter,
1262
1283
  model: responseValidateModel,
@@ -1265,6 +1286,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1265
1286
  maxOutputTokens: responseValidateMaxOutputTokens,
1266
1287
  temperature: responseValidateTemperature
1267
1288
  });
1289
+ timings.response_validation_ms = (timings.response_validation_ms ?? 0) + (Date.now() - validationStartedAt);
1268
1290
  } catch (err) {
1269
1291
  log(`Response validation failed: ${err instanceof Error ? err.message : String(err)}`);
1270
1292
  }
@@ -1306,6 +1328,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1306
1328
  session_recall_count: sessionRecallCount,
1307
1329
  memory_items_upserted: memoryItemsUpserted,
1308
1330
  memory_items_extracted: memoryItemsExtracted,
1331
+ timings: Object.keys(timings).length > 0 ? timings : undefined,
1309
1332
  tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
1310
1333
  latency_ms: latencyMs
1311
1334
  };
@@ -1335,6 +1358,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1335
1358
  const runMemoryExtraction = async () => {
1336
1359
  const extractionMessages = history.slice(-memoryExtractionMaxMessages);
1337
1360
  if (extractionMessages.length === 0) return;
1361
+ const extractionStartedAt = Date.now();
1338
1362
  const extractionPrompt = buildMemoryExtractionPrompt({
1339
1363
  assistantName,
1340
1364
  userId: input.userId,
@@ -1380,6 +1404,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1380
1404
  memoryItemsExtracted += normalizedItems.length;
1381
1405
  memoryItemsUpserted += normalizedItems.length;
1382
1406
  }
1407
+ timings.memory_extraction_ms = (timings.memory_extraction_ms ?? 0) + (Date.now() - extractionStartedAt);
1383
1408
  };
1384
1409
 
1385
1410
  if (memoryExtractionEnabled && (!input.isScheduledTask || memoryExtractScheduled)) {
@@ -1398,6 +1423,12 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1398
1423
 
1399
1424
  // Normalize empty/whitespace-only responses to null
1400
1425
  const finalResult = responseText && responseText.trim() ? responseText : null;
1426
+ if (toolCalls.length > 0) {
1427
+ const totalToolMs = toolCalls.reduce((sum, call) => sum + (call.duration_ms || 0), 0);
1428
+ if (totalToolMs > 0) {
1429
+ timings.tool_ms = totalToolMs;
1430
+ }
1431
+ }
1401
1432
 
1402
1433
  return {
1403
1434
  status: 'success',
@@ -1413,6 +1444,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1413
1444
  session_recall_count: sessionRecallCount,
1414
1445
  memory_items_upserted: memoryItemsUpserted,
1415
1446
  memory_items_extracted: memoryItemsExtracted,
1447
+ timings: Object.keys(timings).length > 0 ? timings : undefined,
1416
1448
  tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
1417
1449
  latency_ms: latencyMs
1418
1450
  };
@@ -39,6 +39,13 @@ type ToolRuntime = {
39
39
  tools: string[];
40
40
  timeoutMs: number;
41
41
  };
42
+ progress: {
43
+ enabled: boolean;
44
+ minIntervalMs: number;
45
+ notifyTools: string[];
46
+ notifyOnStart: boolean;
47
+ notifyOnError: boolean;
48
+ };
42
49
  openrouter: {
43
50
  siteUrl: string;
44
51
  siteName: string;
@@ -86,6 +93,13 @@ function buildToolRuntime(config: AgentRuntimeConfig['agent']): ToolRuntime {
86
93
  tools: toolSummaryTools,
87
94
  timeoutMs: toolSummaryTimeoutMs
88
95
  },
96
+ progress: {
97
+ enabled: config.tools.progress.enabled,
98
+ minIntervalMs: config.tools.progress.minIntervalMs,
99
+ notifyTools: config.tools.progress.notifyTools,
100
+ notifyOnStart: config.tools.progress.notifyOnStart,
101
+ notifyOnError: config.tools.progress.notifyOnError
102
+ },
89
103
  openrouter: {
90
104
  siteUrl: config.openrouter.siteUrl,
91
105
  siteName: config.openrouter.siteName
@@ -824,12 +838,23 @@ async function maybeSummarizeToolResult<T>(name: string, result: T, runtime: Too
824
838
  return payload.apply(limited.text) as T;
825
839
  }
826
840
 
827
- export function createTools(ctx: IpcContext, config: AgentRuntimeConfig['agent'], options?: { onToolCall?: ToolCallLogger; policy?: ToolPolicy }) {
841
+ export function createTools(
842
+ ctx: IpcContext,
843
+ config: AgentRuntimeConfig['agent'],
844
+ options?: { onToolCall?: ToolCallLogger; policy?: ToolPolicy; jobProgress?: { jobId?: string; enabled?: boolean } }
845
+ ) {
828
846
  const runtime = buildToolRuntime(config);
829
847
  const ipc = createIpcHandlers(ctx, config.ipc);
830
848
  const isMain = ctx.isMain;
831
849
  const onToolCall = options?.onToolCall;
832
850
  const policy = options?.policy;
851
+ const progressConfig = runtime.progress;
852
+ const progressJobId = options?.jobProgress?.jobId;
853
+ const progressEnabled = Boolean(progressJobId && progressConfig.enabled && options?.jobProgress?.enabled !== false);
854
+ const progressNotifyTools = new Set(
855
+ (progressConfig.notifyTools || []).map(item => item.trim().toLowerCase()).filter(Boolean)
856
+ );
857
+ let lastProgressNotifyAt = 0;
833
858
  const allowList = (policy?.allow || []).map(item => item.toLowerCase());
834
859
  const denyList = (policy?.deny || []).map(item => item.toLowerCase());
835
860
  const maxPerRunConfig = policy?.max_per_run || {};
@@ -849,24 +874,62 @@ export function createTools(ctx: IpcContext, config: AgentRuntimeConfig['agent']
849
874
  const webFetchAllowlist = runtime.webfetchAllowlist;
850
875
  const webFetchBlocklist = runtime.webfetchBlocklist;
851
876
 
877
+ const shouldNotifyTool = (name: string) => {
878
+ if (!progressEnabled) return false;
879
+ if (!progressNotifyTools || progressNotifyTools.size === 0) return false;
880
+ return progressNotifyTools.has(name.toLowerCase());
881
+ };
882
+
883
+ const sendJobUpdate = (payload: { message: string; level?: 'info' | 'progress' | 'warn' | 'error'; notify?: boolean; data?: Record<string, unknown> }) => {
884
+ if (!progressEnabled || !progressJobId) return;
885
+ void ipc.jobUpdate({
886
+ job_id: progressJobId,
887
+ message: payload.message,
888
+ level: payload.level,
889
+ notify: payload.notify,
890
+ data: payload.data
891
+ }).catch(() => undefined);
892
+ };
893
+
852
894
  const wrapExecute = <TInput, TOutput>(name: string, execute: (args: TInput) => Promise<TOutput>) => {
853
895
  return async (args: TInput): Promise<TOutput> => {
854
896
  const start = Date.now();
897
+ const normalizedName = name.toLowerCase();
898
+ const isSystemTool = normalizedName.startsWith('mcp__');
855
899
  try {
856
- const normalized = name.toLowerCase();
857
- if (denyList.includes(normalized)) {
900
+ if (denyList.includes(normalizedName)) {
858
901
  throw new Error(`Tool is disabled by policy: ${name}`);
859
902
  }
860
- if (allowList.length > 0 && !allowList.includes(normalized)) {
903
+ if (allowList.length > 0 && !allowList.includes(normalizedName)) {
861
904
  throw new Error(`Tool not allowed by policy: ${name}`);
862
905
  }
863
906
  const currentCount = usageCounts.get(name) || 0;
864
- const maxAllowed = maxPerRun.get(normalized) ?? defaultMax;
907
+ const maxAllowed = maxPerRun.get(normalizedName) ?? defaultMax;
865
908
  if (Number.isFinite(maxAllowed) && maxAllowed > 0 && currentCount >= maxAllowed) {
866
909
  throw new Error(`Tool usage limit reached for ${name} (max ${maxAllowed} per run)`);
867
910
  }
868
911
  usageCounts.set(name, currentCount + 1);
869
912
 
913
+ if (!isSystemTool && shouldNotifyTool(name) && progressConfig.notifyOnStart) {
914
+ const now = Date.now();
915
+ if (now - lastProgressNotifyAt >= progressConfig.minIntervalMs) {
916
+ lastProgressNotifyAt = now;
917
+ sendJobUpdate({
918
+ message: `Running ${name}...`,
919
+ level: 'progress',
920
+ notify: true,
921
+ data: { tool: name, stage: 'start' }
922
+ });
923
+ } else {
924
+ sendJobUpdate({
925
+ message: `Running ${name}...`,
926
+ level: 'progress',
927
+ notify: false,
928
+ data: { tool: name, stage: 'start' }
929
+ });
930
+ }
931
+ }
932
+
870
933
  const rawResult = await execute(args);
871
934
  const result = await maybeSummarizeToolResult(name, rawResult, runtime);
872
935
  let outputBytes: number | undefined;
@@ -888,6 +951,14 @@ export function createTools(ctx: IpcContext, config: AgentRuntimeConfig['agent']
888
951
  output_bytes: outputBytes,
889
952
  output_truncated: outputTruncated
890
953
  });
954
+ if (!isSystemTool && shouldNotifyTool(name)) {
955
+ sendJobUpdate({
956
+ message: `${name} finished.`,
957
+ level: 'info',
958
+ notify: false,
959
+ data: { tool: name, stage: 'end', ok: true, duration_ms: Date.now() - start }
960
+ });
961
+ }
891
962
  return result;
892
963
  } catch (err) {
893
964
  onToolCall?.({
@@ -897,6 +968,14 @@ export function createTools(ctx: IpcContext, config: AgentRuntimeConfig['agent']
897
968
  duration_ms: Date.now() - start,
898
969
  error: err instanceof Error ? err.message : String(err)
899
970
  });
971
+ if (!isSystemTool && shouldNotifyTool(name) && progressConfig.notifyOnError) {
972
+ sendJobUpdate({
973
+ message: `${name} failed: ${err instanceof Error ? err.message : String(err)}`,
974
+ level: 'error',
975
+ notify: true,
976
+ data: { tool: name, stage: 'end', ok: false, duration_ms: Date.now() - start }
977
+ });
978
+ }
900
979
  throw err;
901
980
  }
902
981
  };
@@ -30,6 +30,10 @@ export type AgentContext = {
30
30
  tokenEstimate: ReturnType<typeof getTokenEstimateConfig>;
31
31
  modelCapabilities: ModelCapabilities;
32
32
  dynamicMemoryBudget: number;
33
+ timings: {
34
+ context_build_ms?: number;
35
+ memory_recall_ms?: number;
36
+ };
33
37
  };
34
38
  export declare function buildAgentContext(params: {
35
39
  groupFolder: string;
@@ -39,5 +43,6 @@ export declare function buildAgentContext(params: {
39
43
  recallMaxTokens: number;
40
44
  toolAllow?: string[];
41
45
  toolDeny?: string[];
46
+ recallEnabled?: boolean;
42
47
  }): Promise<AgentContext>;
43
48
  //# sourceMappingURL=agent-context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-context.d.ts","sourceRoot":"","sources":["../src/agent-context.ts"],"names":[],"mappings":"AAGA,OAAO,EAA+C,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG3F,OAAO,EAAgB,iBAAiB,EAAE,sBAAsB,EAAE,eAAe,EAAwB,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGxJ,MAAM,MAAM,YAAY,GAAG;IACzB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5E,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,UAAU,EAAE,UAAU,CAAC;IACvB,eAAe,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAC9G,aAAa,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE;YAAE,cAAc,CAAC,EAAE,MAAM,CAAC;YAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;IAC3H,aAAa,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;IACpD,YAAY,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;IACjD,aAAa,EAAE,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC;IACzD,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,mBAAmB,EAAE,MAAM,CAAC;CAC7B,CAAC;AA4BF,wBAAsB,iBAAiB,CAAC,MAAM,EAAE;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB,GAAG,OAAO,CAAC,YAAY,CAAC,CAwFxB"}
1
+ {"version":3,"file":"agent-context.d.ts","sourceRoot":"","sources":["../src/agent-context.ts"],"names":[],"mappings":"AAGA,OAAO,EAA+C,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG3F,OAAO,EAAgB,iBAAiB,EAAE,sBAAsB,EAAE,eAAe,EAAwB,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGxJ,MAAM,MAAM,YAAY,GAAG;IACzB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5E,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,UAAU,EAAE,UAAU,CAAC;IACvB,eAAe,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAC9G,aAAa,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE;YAAE,cAAc,CAAC,EAAE,MAAM,CAAC;YAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;IAC3H,aAAa,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;IACpD,YAAY,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;IACjD,aAAa,EAAE,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC;IACzD,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,OAAO,EAAE;QACP,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC3B,CAAC;CACH,CAAC;AA4BF,wBAAsB,iBAAiB,CAAC,MAAM,EAAE;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,GAAG,OAAO,CAAC,YAAY,CAAC,CAmGxB"}
@@ -22,6 +22,7 @@ function calculateMemoryBudget(modelCapabilities, maxOutputTokens, configuredMax
22
22
  return Math.min(Math.max(800, Math.min(4000, calculatedBudget)), configuredMax);
23
23
  }
24
24
  export async function buildAgentContext(params) {
25
+ const startedAt = Date.now();
25
26
  const runtime = loadRuntimeConfig();
26
27
  const defaultModel = runtime.host.defaultModel;
27
28
  const modelRegistry = loadModelRegistry(defaultModel);
@@ -36,13 +37,19 @@ export async function buildAgentContext(params) {
36
37
  const modelCapabilities = await getModelCapabilities(resolvedModel.model);
37
38
  // Calculate dynamic memory budget based on model context window
38
39
  const dynamicMemoryBudget = calculateMemoryBudget(modelCapabilities, runtime.agent.context.maxOutputTokens, params.recallMaxTokens);
39
- const memoryRecall = await buildHybridMemoryRecall({
40
- groupFolder: params.groupFolder,
41
- userId: params.userId ?? null,
42
- query: params.recallQuery,
43
- maxResults: params.recallMaxResults,
44
- maxTokens: dynamicMemoryBudget
45
- });
40
+ let memoryRecall = [];
41
+ let memoryRecallMs;
42
+ if (params.recallEnabled !== false && params.recallMaxResults > 0 && params.recallMaxTokens > 0) {
43
+ const recallStart = Date.now();
44
+ memoryRecall = await buildHybridMemoryRecall({
45
+ groupFolder: params.groupFolder,
46
+ userId: params.userId ?? null,
47
+ query: params.recallQuery,
48
+ maxResults: params.recallMaxResults,
49
+ maxTokens: dynamicMemoryBudget
50
+ });
51
+ memoryRecallMs = Date.now() - recallStart;
52
+ }
46
53
  const userProfile = buildUserProfile({
47
54
  groupFolder: params.groupFolder,
48
55
  userId: params.userId ?? null
@@ -93,7 +100,11 @@ export async function buildAgentContext(params) {
93
100
  modelPricing,
94
101
  tokenEstimate,
95
102
  modelCapabilities,
96
- dynamicMemoryBudget
103
+ dynamicMemoryBudget,
104
+ timings: {
105
+ context_build_ms: Date.now() - startedAt,
106
+ memory_recall_ms: memoryRecallMs
107
+ }
97
108
  };
98
109
  }
99
110
  //# sourceMappingURL=agent-context.js.map