salmon-loop 0.3.2 → 0.4.1
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/dist/cli/authorization/non-interactive.js +9 -13
- package/dist/cli/chat.js +12 -6
- package/dist/cli/commands/allowlist.js +1 -1
- package/dist/cli/commands/chat.js +13 -13
- package/dist/cli/commands/parallel.js +1 -1
- package/dist/cli/commands/run/handler.js +6 -3
- package/dist/cli/commands/run/loop-params.js +1 -0
- package/dist/cli/commands/run/parse-options.js +14 -26
- package/dist/cli/commands/run/runtime-llm.js +15 -12
- package/dist/cli/headless/openai-responses-canonical-applier.js +1 -7
- package/dist/cli/reporters/standard.js +2 -3
- package/dist/cli/reporters/stream-json.js +2 -1
- package/dist/cli/slash/runtime.js +2 -2
- package/dist/cli/ui/hooks/useLoopEvents.js +1 -1
- package/dist/cli/ui/hooks/useLoopState.js +1 -1
- package/dist/core/ast/parser.js +18 -9
- package/dist/core/config/schema.js +738 -0
- package/dist/core/config/validate.js +11 -922
- package/dist/core/context/gatherers/ast-gatherer.js +4 -12
- package/dist/core/context/gatherers/ghost-dependency-gatherer.js +0 -1
- package/dist/core/context/gatherers/knowledge-gatherer.js +3 -0
- package/dist/core/context/service.js +8 -0
- package/dist/core/context/token/encoding-registry.js +7 -6
- package/dist/core/extensions/index.js +48 -3
- package/dist/core/extensions/load.js +3 -2
- package/dist/core/extensions/merge.js +5 -1
- package/dist/core/extensions/paths.js +6 -0
- package/dist/core/extensions/schemas.js +21 -0
- package/dist/core/facades/cli-command-chat.js +2 -0
- package/dist/core/facades/cli-run-handler.js +1 -0
- package/dist/core/facades/cli-utils-serialize.js +2 -0
- package/dist/core/grizzco/dsl/llm-strategy.js +3 -2
- package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +15 -10
- package/dist/core/grizzco/engine/pipeline/pipeline.js +149 -240
- package/dist/core/grizzco/engine/transaction/attempt-failure.js +5 -4
- package/dist/core/grizzco/engine/transaction/authorization-summary.js +2 -1
- package/dist/core/grizzco/runtime/apply-back-runtime.js +2 -1
- package/dist/core/grizzco/services/registry.js +18 -0
- package/dist/core/grizzco/steps/audit.js +20 -10
- package/dist/core/grizzco/steps/display-report.js +4 -11
- package/dist/core/grizzco/steps/explore.js +9 -2
- package/dist/core/grizzco/steps/patch/prompt-input.js +4 -1
- package/dist/core/grizzco/steps/patch.js +1 -0
- package/dist/core/grizzco/steps/plan.js +58 -49
- package/dist/core/grizzco/steps/tool-runtime.js +3 -0
- package/dist/core/grizzco/workers/strata-sync-worker.js +2 -1
- package/dist/core/llm/ai-sdk/message-mapper.js +24 -18
- package/dist/core/llm/ai-sdk/request-params.js +1 -3
- package/dist/core/llm/ai-sdk/result-mapper.js +14 -8
- package/dist/core/llm/ai-sdk/retry-classifier.js +6 -4
- package/dist/core/llm/contracts/repair.js +16 -8
- package/dist/core/llm/errors.js +13 -10
- package/dist/core/llm/output-policy.js +8 -0
- package/dist/core/llm/redact.js +1 -3
- package/dist/core/llm/sub-agent-factory.js +48 -0
- package/dist/core/llm/tool-calling-stub.js +48 -0
- package/dist/core/llm/utils.js +17 -6
- package/dist/core/mcp/bridge/prompt-command-provider.js +4 -3
- package/dist/core/mcp/bridge/tool-bridge.js +5 -14
- package/dist/core/mcp/client/connection-manager.js +3 -2
- package/dist/core/mcp/host/sampling-provider.js +1 -1
- package/dist/core/mcp/schema/json-schema-to-zod.js +2 -1
- package/dist/core/memory/relevant-retrieval.js +6 -4
- package/dist/core/observability/authorization-decisions.js +13 -12
- package/dist/core/observability/error-mapping.js +2 -1
- package/dist/core/observability/token-usage.js +5 -4
- package/dist/core/plugin/loader.js +5 -4
- package/dist/core/prompts/registry.js +11 -29
- package/dist/core/protocols/a2a/sdk/server.js +2 -3
- package/dist/core/protocols/acp/formal-agent.js +10 -4
- package/dist/core/protocols/acp/stdio-server.js +6 -6
- package/dist/core/runtime/agent-server-runtime.js +3 -2
- package/dist/core/runtime/initialize.js +70 -6
- package/dist/core/session/compaction/index.js +4 -3
- package/dist/core/session/manager.js +24 -37
- package/dist/core/session/token-tracker.js +18 -7
- package/dist/core/skills/parser.js +3 -2
- package/dist/core/skills/runtime/MicroTaskRunner.js +1 -1
- package/dist/core/skills/runtime/SkillRunner.js +5 -2
- package/dist/core/slash/steps/slash-execute.js +7 -5
- package/dist/core/slash/strategy.js +1 -1
- package/dist/core/strata/layers/worktree.js +7 -9
- package/dist/core/strata/runtime/synchronizer.js +10 -9
- package/dist/core/streaming/canonical/parts-from-llm-stream-chunk.js +1 -11
- package/dist/core/structured-output/json-schema-validator.js +1 -13
- package/dist/core/sub-agent/context-snapshot.js +12 -6
- package/dist/core/sub-agent/controller.js +70 -1
- package/dist/core/sub-agent/core/loop.js +25 -3
- package/dist/core/sub-agent/core/manager.js +319 -116
- package/dist/core/sub-agent/registry-defaults.js +12 -0
- package/dist/core/sub-agent/registry.js +8 -0
- package/dist/core/sub-agent/team.js +98 -0
- package/dist/core/sub-agent/tools/task-await.js +109 -0
- package/dist/core/sub-agent/tools/task-spawn.js +49 -7
- package/dist/core/sub-agent/tools/team.js +92 -0
- package/dist/core/sub-agent/types.js +11 -2
- package/dist/core/tools/budget.js +4 -11
- package/dist/core/tools/builtin/code-search/executor.js +46 -43
- package/dist/core/tools/builtin/fs.js +14 -6
- package/dist/core/tools/builtin/index.js +41 -107
- package/dist/core/tools/builtin/interaction.js +13 -15
- package/dist/core/tools/builtin/proposal.js +11 -2
- package/dist/core/tools/capability/executor.js +5 -5
- package/dist/core/tools/headless-payload.js +1 -3
- package/dist/core/tools/mapper.js +8 -42
- package/dist/core/tools/parallel/persistence.js +17 -5
- package/dist/core/tools/parallel/scheduler.js +23 -21
- package/dist/core/tools/permissions/permission-rules.js +66 -114
- package/dist/core/tools/plugins/loader.js +4 -3
- package/dist/core/tools/router.js +24 -53
- package/dist/core/tools/session.js +54 -97
- package/dist/core/tools/streaming/ToolCallAccumulator.js +1 -3
- package/dist/core/tools/tool-visibility.js +2 -1
- package/dist/core/tools/types.js +10 -0
- package/dist/core/utils/error.js +79 -0
- package/dist/core/utils/serialize.js +63 -0
- package/dist/core/utils/zod.js +29 -0
- package/dist/core/workspace/capabilities.js +3 -2
- package/dist/integrations/langfuse/litellm-langfuse-outcome-reporter.js +9 -8
- package/dist/locales/en.js +2 -1
- package/package.json +1 -1
|
@@ -268,34 +268,11 @@ export class ChatSessionManager {
|
|
|
268
268
|
* List all sessions (sorted by update time)
|
|
269
269
|
*/
|
|
270
270
|
async listSessions() {
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const promises = chunk.map(async (file) => {
|
|
277
|
-
try {
|
|
278
|
-
const filePath = join(this.storageDir, file);
|
|
279
|
-
const data = await this.fileAdapter.readFile(filePath);
|
|
280
|
-
const session = JSON.parse(data);
|
|
281
|
-
return {
|
|
282
|
-
id: session.meta.id,
|
|
283
|
-
name: session.meta.name,
|
|
284
|
-
updatedAt: session.meta.updatedAt,
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
catch (error) {
|
|
288
|
-
getLogger().warn(`Failed to list session file ${file}: ${error}`);
|
|
289
|
-
return null;
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
const results = await Promise.all(promises);
|
|
293
|
-
for (const result of results) {
|
|
294
|
-
if (result) {
|
|
295
|
-
sessions.push(result);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
271
|
+
const sessions = await this.scanSessionFiles((session) => ({
|
|
272
|
+
id: session.meta.id,
|
|
273
|
+
name: session.meta.name,
|
|
274
|
+
updatedAt: session.meta.updatedAt,
|
|
275
|
+
}));
|
|
299
276
|
return sessions.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
300
277
|
}
|
|
301
278
|
/**
|
|
@@ -344,9 +321,22 @@ export class ChatSessionManager {
|
|
|
344
321
|
* Load all sessions from storage
|
|
345
322
|
*/
|
|
346
323
|
async loadAllSessions() {
|
|
324
|
+
return this.scanSessionFiles((session) => {
|
|
325
|
+
session.meta.chatState = normalizeChatState(session.meta.chatState);
|
|
326
|
+
session.meta.artifactState = normalizeSessionArtifactState(session.meta.artifactState);
|
|
327
|
+
session.meta.replacementState = normalizeToolResultReplacementState(session.meta.replacementState);
|
|
328
|
+
return session;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Shared scan-and-parse for session files.
|
|
333
|
+
* Reads JSON files from storageDir in chunks, parses each, and maps via the provided callback.
|
|
334
|
+
* Silently skips files that fail to parse.
|
|
335
|
+
*/
|
|
336
|
+
async scanSessionFiles(mapFn) {
|
|
347
337
|
const files = await this.fileAdapter.readdir(this.storageDir).catch(() => []);
|
|
348
338
|
const jsonFiles = files.filter((f) => f.endsWith('.json'));
|
|
349
|
-
const
|
|
339
|
+
const results = [];
|
|
350
340
|
for (let i = 0; i < jsonFiles.length; i += ChatSessionManager.FILE_READ_CHUNK_SIZE) {
|
|
351
341
|
const chunk = jsonFiles.slice(i, i + ChatSessionManager.FILE_READ_CHUNK_SIZE);
|
|
352
342
|
const promises = chunk.map(async (file) => {
|
|
@@ -354,24 +344,21 @@ export class ChatSessionManager {
|
|
|
354
344
|
const filePath = join(this.storageDir, file);
|
|
355
345
|
const data = await this.fileAdapter.readFile(filePath);
|
|
356
346
|
const session = JSON.parse(data);
|
|
357
|
-
|
|
358
|
-
session.meta.artifactState = normalizeSessionArtifactState(session.meta.artifactState);
|
|
359
|
-
session.meta.replacementState = normalizeToolResultReplacementState(session.meta.replacementState);
|
|
360
|
-
return session;
|
|
347
|
+
return mapFn(session);
|
|
361
348
|
}
|
|
362
349
|
catch (error) {
|
|
363
350
|
getLogger().warn(`Failed to load session file ${file}: ${error}`);
|
|
364
351
|
return null;
|
|
365
352
|
}
|
|
366
353
|
});
|
|
367
|
-
const
|
|
368
|
-
for (const result of
|
|
354
|
+
const chunkResults = await Promise.all(promises);
|
|
355
|
+
for (const result of chunkResults) {
|
|
369
356
|
if (result) {
|
|
370
|
-
|
|
357
|
+
results.push(result);
|
|
371
358
|
}
|
|
372
359
|
}
|
|
373
360
|
}
|
|
374
|
-
return
|
|
361
|
+
return results;
|
|
375
362
|
}
|
|
376
363
|
/**
|
|
377
364
|
* Delete a session file
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { FileAdapter } from '../adapters/fs/index.js';
|
|
3
3
|
import { logIgnoredError } from '../observability/ignored-error.js';
|
|
4
|
+
import { isRecord } from '../utils/serialize.js';
|
|
4
5
|
/**
|
|
5
6
|
* Token usage tracker for chat sessions.
|
|
6
7
|
* Extracts and accumulates token statistics from LLM execution results.
|
|
@@ -21,26 +22,36 @@ export class TokenTracker {
|
|
|
21
22
|
try {
|
|
22
23
|
const auditRaw = await this.fileAdapter.readFile(result.auditPath, 'utf8');
|
|
23
24
|
const audit = JSON.parse(auditRaw);
|
|
24
|
-
|
|
25
|
+
if (!isRecord(audit))
|
|
26
|
+
return null;
|
|
27
|
+
const context = isRecord(audit.context) ? audit.context : null;
|
|
28
|
+
const eventsRef = isRecord(context?.eventsRef) ? context.eventsRef : null;
|
|
25
29
|
if (!eventsRef || typeof eventsRef.path !== 'string')
|
|
26
30
|
return null;
|
|
27
31
|
const eventsPath = path.isAbsolute(eventsRef.path)
|
|
28
32
|
? eventsRef.path
|
|
29
33
|
: path.join(path.dirname(result.auditPath), eventsRef.path);
|
|
30
34
|
const eventsRaw = await this.fileAdapter.readFile(eventsPath, 'utf8');
|
|
31
|
-
const events =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
const events = [];
|
|
36
|
+
for (const line of eventsRaw.split('\n')) {
|
|
37
|
+
if (line.trim().length === 0)
|
|
38
|
+
continue;
|
|
39
|
+
try {
|
|
40
|
+
events.push(JSON.parse(line));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Skip malformed lines
|
|
44
|
+
}
|
|
45
|
+
}
|
|
35
46
|
let inputTokens = 0;
|
|
36
47
|
let outputTokens = 0;
|
|
37
48
|
for (const event of events) {
|
|
38
|
-
if (!event
|
|
49
|
+
if (!isRecord(event))
|
|
39
50
|
continue;
|
|
40
51
|
if (event.action !== 'llm.usage')
|
|
41
52
|
continue;
|
|
42
53
|
const details = event.details;
|
|
43
|
-
if (!details
|
|
54
|
+
if (!isRecord(details))
|
|
44
55
|
continue;
|
|
45
56
|
const promptTokens = details.promptTokens;
|
|
46
57
|
const completionTokens = details.completionTokens;
|
|
@@ -3,6 +3,7 @@ import { parse as parseYaml } from 'yaml';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { text } from '../../locales/index.js';
|
|
5
5
|
import { tryGetLogger } from '../observability/logger.js';
|
|
6
|
+
import { errorMessage } from '../utils/error.js';
|
|
6
7
|
/**
|
|
7
8
|
* Safe logger accessor that never throws when the logger is not yet initialized.
|
|
8
9
|
*
|
|
@@ -129,7 +130,7 @@ export class SkillParser {
|
|
|
129
130
|
parsed = parseYaml(yamlRaw);
|
|
130
131
|
}
|
|
131
132
|
catch (error) {
|
|
132
|
-
const reason =
|
|
133
|
+
const reason = errorMessage(error);
|
|
133
134
|
const msg = text.skills.yamlParseError(filePath, reason);
|
|
134
135
|
safeLogger().error(msg);
|
|
135
136
|
throw new Error(msg);
|
|
@@ -188,7 +189,7 @@ export class SkillParser {
|
|
|
188
189
|
parsed = parseYaml(yamlRaw);
|
|
189
190
|
}
|
|
190
191
|
catch (error) {
|
|
191
|
-
const reason =
|
|
192
|
+
const reason = errorMessage(error);
|
|
192
193
|
const msg = text.skills.yamlParseError(filePath, reason);
|
|
193
194
|
safeLogger().error(msg);
|
|
194
195
|
throw new Error(msg);
|
|
@@ -2,6 +2,7 @@ import * as crypto from 'crypto';
|
|
|
2
2
|
import { MicroTaskRunner } from '../../grizzco/dsl/MicroTaskRunner.js';
|
|
3
3
|
import { tryGetLogger } from '../../observability/logger.js';
|
|
4
4
|
import { Phase } from '../../types/index.js';
|
|
5
|
+
import { isRecord } from '../../utils/serialize.js';
|
|
5
6
|
import { emitSkillAuditEvent, generateSkillTraceId, hashSkillArgs } from '../audit.js';
|
|
6
7
|
import { SkillParser } from '../parser.js';
|
|
7
8
|
import { SkillStrategyDSL } from '../strategy.js';
|
|
@@ -227,7 +228,9 @@ export async function executeSkill(options) {
|
|
|
227
228
|
argsHash,
|
|
228
229
|
traceId,
|
|
229
230
|
denyReason: result.error?.code || 'unknown',
|
|
230
|
-
denySource: result.meta
|
|
231
|
+
denySource: isRecord(result.meta) && isRecord(result.meta.authorization)
|
|
232
|
+
? result.meta.authorization.source
|
|
233
|
+
: 'policy',
|
|
231
234
|
durationMs: Date.now() - startedAt,
|
|
232
235
|
});
|
|
233
236
|
}
|
|
@@ -264,7 +267,7 @@ export async function executeSkill(options) {
|
|
|
264
267
|
cmd,
|
|
265
268
|
output: String(output),
|
|
266
269
|
})),
|
|
267
|
-
injectedPrompt: String(inject?.params
|
|
270
|
+
injectedPrompt: String((isRecord(inject?.params) ? inject.params.prompt : undefined) ?? ''),
|
|
268
271
|
status,
|
|
269
272
|
};
|
|
270
273
|
}
|
|
@@ -2,12 +2,13 @@ function planToDecision(plan, options) {
|
|
|
2
2
|
const action = plan.actions[0];
|
|
3
3
|
if (!action)
|
|
4
4
|
return { kind: 'consumed' };
|
|
5
|
+
const p = action.params ?? {};
|
|
5
6
|
if (action.type === 'FORWARD_TEXT') {
|
|
6
|
-
const next = String(
|
|
7
|
+
const next = String(p.input ?? '');
|
|
7
8
|
return { kind: 'forward', input: next };
|
|
8
9
|
}
|
|
9
10
|
if (action.type === 'UNKNOWN_SLASH') {
|
|
10
|
-
const cmd = String(
|
|
11
|
+
const cmd = String(p.commandName ?? '');
|
|
11
12
|
if (options.unknownSlashPolicy === 'forward_as_text') {
|
|
12
13
|
return { kind: 'forward', input: cmd };
|
|
13
14
|
}
|
|
@@ -31,7 +32,8 @@ export function buildSlashExecuteStep(options, meta) {
|
|
|
31
32
|
const decision = planToDecision(plan, { unknownSlashPolicy: options.unknownSlashPolicy });
|
|
32
33
|
return { ...context, data: { ...data, __decision: decision } };
|
|
33
34
|
}
|
|
34
|
-
const
|
|
35
|
+
const p = action.params ?? {};
|
|
36
|
+
const commandName = String(p.commandName ?? '');
|
|
35
37
|
const spec = options.registry.find(commandName);
|
|
36
38
|
if (!spec) {
|
|
37
39
|
const decision = planToDecision({ ...plan, actions: [{ type: 'UNKNOWN_SLASH', params: { commandName } }] }, { unknownSlashPolicy: options.unknownSlashPolicy });
|
|
@@ -50,8 +52,8 @@ export function buildSlashExecuteStep(options, meta) {
|
|
|
50
52
|
const req = {
|
|
51
53
|
rawInput: context.input.raw,
|
|
52
54
|
command: spec,
|
|
53
|
-
argsText: String(
|
|
54
|
-
tokens: Array.isArray(
|
|
55
|
+
argsText: String(p.argsText ?? ''),
|
|
56
|
+
tokens: Array.isArray(p.tokens) ? p.tokens : [],
|
|
55
57
|
meta,
|
|
56
58
|
};
|
|
57
59
|
const result = await handler.execute(req);
|
|
@@ -18,7 +18,7 @@ export const SlashStrategyDSL = (engine) => {
|
|
|
18
18
|
.when((c) => c.input.isSlash && Boolean(c.resolved?.command), (p) => {
|
|
19
19
|
p.setWorker('slash.execute');
|
|
20
20
|
p.addAction('EXECUTE_SLASH', {
|
|
21
|
-
commandName: engine.ctx.resolved
|
|
21
|
+
commandName: engine.ctx.resolved?.command?.name ?? '',
|
|
22
22
|
argsText: engine.ctx.input.argsText ?? '',
|
|
23
23
|
tokens: engine.ctx.input.tokens ?? [],
|
|
24
24
|
});
|
|
@@ -5,7 +5,9 @@ import { text } from '../../../locales/index.js';
|
|
|
5
5
|
import { access, readdir, realpath, rm } from '../../adapters/fs/node-fs.js';
|
|
6
6
|
import { GitAdapter } from '../../adapters/git/git-adapter.js';
|
|
7
7
|
import { getLogger } from '../../observability/logger.js';
|
|
8
|
+
import { errorMessage } from '../../utils/error.js';
|
|
8
9
|
import { isPathWithinDirectory, normalizePath } from '../../utils/path.js';
|
|
10
|
+
import { isRecord } from '../../utils/serialize.js';
|
|
9
11
|
import { detectDependencyPaths } from './shadow-driver/strategy.js';
|
|
10
12
|
function resolveEnvironmentMode(options) {
|
|
11
13
|
return options.environmentMode === 'parity' ? 'parity' : 'strict';
|
|
@@ -73,14 +75,14 @@ async function removeProjectedWorktreeEntries(workPath) {
|
|
|
73
75
|
worktreeRealPath = await realpath(workPath);
|
|
74
76
|
}
|
|
75
77
|
catch (error) {
|
|
76
|
-
throw new Error(`Failed to resolve worktree path before git cleanup (${workPath}): ${
|
|
78
|
+
throw new Error(`Failed to resolve worktree path before git cleanup (${workPath}): ${errorMessage(error)}`);
|
|
77
79
|
}
|
|
78
80
|
let entries = [];
|
|
79
81
|
try {
|
|
80
82
|
entries = (await readdir(workPath, { withFileTypes: true }));
|
|
81
83
|
}
|
|
82
84
|
catch (error) {
|
|
83
|
-
throw new Error(`Failed to enumerate worktree entries before git cleanup (${workPath}): ${
|
|
85
|
+
throw new Error(`Failed to enumerate worktree entries before git cleanup (${workPath}): ${errorMessage(error)}`);
|
|
84
86
|
}
|
|
85
87
|
for (const entry of entries) {
|
|
86
88
|
const name = entry?.name;
|
|
@@ -116,7 +118,7 @@ async function pruneWorktreeDependencyRoots(baseRepoPath, worktreePath) {
|
|
|
116
118
|
getLogger().debug(`Pruned disposable dependency root before worktree cleanup: ${dependencyRoot}`);
|
|
117
119
|
}
|
|
118
120
|
catch (error) {
|
|
119
|
-
getLogger().debug(`Failed to prune dependency root before worktree cleanup (${dependencyRoot}): ${
|
|
121
|
+
getLogger().debug(`Failed to prune dependency root before worktree cleanup (${dependencyRoot}): ${errorMessage(error)}`);
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
}
|
|
@@ -243,7 +245,7 @@ export class WorkspaceManager {
|
|
|
243
245
|
return true;
|
|
244
246
|
}
|
|
245
247
|
catch (error) {
|
|
246
|
-
if (error &&
|
|
248
|
+
if (isRecord(error) && error.code === 'ENOENT') {
|
|
247
249
|
return false;
|
|
248
250
|
}
|
|
249
251
|
return true;
|
|
@@ -268,11 +270,7 @@ export class WorkspaceManager {
|
|
|
268
270
|
}
|
|
269
271
|
}
|
|
270
272
|
catch (error) {
|
|
271
|
-
const msg = error
|
|
272
|
-
? error instanceof Error
|
|
273
|
-
? error.message
|
|
274
|
-
: String(error)
|
|
275
|
-
: String(error);
|
|
273
|
+
const msg = errorMessage(error);
|
|
276
274
|
onEvent?.({
|
|
277
275
|
type: 'action.fallback',
|
|
278
276
|
tool: 'git',
|
|
@@ -8,6 +8,7 @@ import { GitAdapter } from '../../adapters/git/git-adapter.js';
|
|
|
8
8
|
import { logIgnoredError } from '../../observability/ignored-error.js';
|
|
9
9
|
import { getLogger } from '../../observability/logger.js';
|
|
10
10
|
import { getMonitor } from '../../observability/monitor.js';
|
|
11
|
+
import { errorMessage } from '../../utils/error.js';
|
|
11
12
|
import { isCanonicalPathWithinDirectory } from '../../utils/path.js';
|
|
12
13
|
import { detectDependencyPaths } from '../layers/shadow-driver/strategy.js';
|
|
13
14
|
const SECURITY_BLOCKLIST = [
|
|
@@ -135,7 +136,7 @@ export class WorkspaceSynchronizer {
|
|
|
135
136
|
detectedDependencyPaths = await detectDependencyPaths(repoPath);
|
|
136
137
|
}
|
|
137
138
|
catch (error) {
|
|
138
|
-
getLogger().debug(`[checkpoint] Failed to detect dependency paths: ${
|
|
139
|
+
getLogger().debug(`[checkpoint] Failed to detect dependency paths: ${errorMessage(error)}`);
|
|
139
140
|
}
|
|
140
141
|
const candidates = new Set([
|
|
141
142
|
...DEFAULT_DEPENDENCY_ROOT_CANDIDATES,
|
|
@@ -445,7 +446,7 @@ export class WorkspaceSynchronizer {
|
|
|
445
446
|
});
|
|
446
447
|
}
|
|
447
448
|
catch (error) {
|
|
448
|
-
throw new Error(`Apply-back completed with conflicts (Atomic Patch). Rejection files (.rej) have been generated. Original error: ${
|
|
449
|
+
throw new Error(`Apply-back completed with conflicts (Atomic Patch). Rejection files (.rej) have been generated. Original error: ${errorMessage(error)}`);
|
|
449
450
|
}
|
|
450
451
|
}
|
|
451
452
|
parseStatusEntries(statusPorcelainZ) {
|
|
@@ -624,7 +625,7 @@ export class WorkspaceSynchronizer {
|
|
|
624
625
|
stagedPatchPath,
|
|
625
626
|
};
|
|
626
627
|
};
|
|
627
|
-
dirtyBackup =
|
|
628
|
+
dirtyBackup = await createDirtyBackup();
|
|
628
629
|
getLogger().info(text.loop.applyBackCheckpointCreated());
|
|
629
630
|
getLogger().info(text.loop.applyBackCheckpointLocation(dirtyBackup?.dir || ''));
|
|
630
631
|
if (telemetry) {
|
|
@@ -673,7 +674,7 @@ export class WorkspaceSynchronizer {
|
|
|
673
674
|
}
|
|
674
675
|
}
|
|
675
676
|
catch (error) {
|
|
676
|
-
const err = error instanceof Error ? error : new Error(
|
|
677
|
+
const err = error instanceof Error ? error : new Error(errorMessage(error));
|
|
677
678
|
if (telemetry) {
|
|
678
679
|
telemetry.error = err.message;
|
|
679
680
|
}
|
|
@@ -737,7 +738,7 @@ export class WorkspaceSynchronizer {
|
|
|
737
738
|
await copyFile(path.join(trackedDir, ...file.split('/')), path.join(mainRepoPath, ...file.split('/')));
|
|
738
739
|
}
|
|
739
740
|
catch (e) {
|
|
740
|
-
getLogger().error(`[applyBack] Failed to restore tracked file ${file}: ${
|
|
741
|
+
getLogger().error(`[applyBack] Failed to restore tracked file ${file}: ${errorMessage(e)}`);
|
|
741
742
|
}
|
|
742
743
|
}
|
|
743
744
|
}
|
|
@@ -769,7 +770,7 @@ export class WorkspaceSynchronizer {
|
|
|
769
770
|
}
|
|
770
771
|
}
|
|
771
772
|
catch (e) {
|
|
772
|
-
const patchError =
|
|
773
|
+
const patchError = errorMessage(e);
|
|
773
774
|
getLogger().error(`[applyBack] Failed to restore staged state from patch. ${patchError}. ` +
|
|
774
775
|
`Falling back to read-tree restore.`);
|
|
775
776
|
try {
|
|
@@ -780,7 +781,7 @@ export class WorkspaceSynchronizer {
|
|
|
780
781
|
}
|
|
781
782
|
}
|
|
782
783
|
catch (fallbackError) {
|
|
783
|
-
const fallbackMessage =
|
|
784
|
+
const fallbackMessage = errorMessage(fallbackError);
|
|
784
785
|
if (telemetry) {
|
|
785
786
|
telemetry.stagedRestoreSucceeded = false;
|
|
786
787
|
telemetry.stagedRestoreError = `${patchError}; fallback read-tree failed: ${fallbackMessage}`;
|
|
@@ -808,7 +809,7 @@ export class WorkspaceSynchronizer {
|
|
|
808
809
|
catch (snapshotRestoreError) {
|
|
809
810
|
getLogger().error(`[applyBack] Snapshot restore failed during clean rollback. ` +
|
|
810
811
|
`baseRef=${checkpointRef.baseRef}; ` +
|
|
811
|
-
`error=${
|
|
812
|
+
`error=${errorMessage(snapshotRestoreError)}. ` +
|
|
812
813
|
`Falling back to clean reset.`);
|
|
813
814
|
}
|
|
814
815
|
if (!restoredFromSnapshot) {
|
|
@@ -830,7 +831,7 @@ export class WorkspaceSynchronizer {
|
|
|
830
831
|
await rm(dirtyBackup.dir, { recursive: true, force: true });
|
|
831
832
|
}
|
|
832
833
|
catch (cleanupError) {
|
|
833
|
-
getLogger().debug(`[applyBack] Failed to cleanup dirty backup ${dirtyBackup.dir}: ${
|
|
834
|
+
getLogger().debug(`[applyBack] Failed to cleanup dirty backup ${dirtyBackup.dir}: ${errorMessage(cleanupError)}`);
|
|
834
835
|
}
|
|
835
836
|
}
|
|
836
837
|
if (telemetry) {
|
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
3
|
-
}
|
|
4
|
-
function getString(record, key) {
|
|
5
|
-
const value = record[key];
|
|
6
|
-
return typeof value === 'string' ? value : null;
|
|
7
|
-
}
|
|
8
|
-
function getRecord(record, key) {
|
|
9
|
-
const value = record[key];
|
|
10
|
-
return isRecord(value) ? value : null;
|
|
11
|
-
}
|
|
1
|
+
import { isRecord, getString, getRecord } from '../../utils/serialize.js';
|
|
12
2
|
/**
|
|
13
3
|
* Best-effort conversion from our provider-agnostic `LLMStreamChunk` into
|
|
14
4
|
* provider-agnostic canonical stream parts.
|
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
import { Ajv } from 'ajv';
|
|
2
|
-
|
|
3
|
-
try {
|
|
4
|
-
return JSON.stringify(value);
|
|
5
|
-
}
|
|
6
|
-
catch {
|
|
7
|
-
try {
|
|
8
|
-
return String(value);
|
|
9
|
-
}
|
|
10
|
-
catch {
|
|
11
|
-
return '[Unserializable]';
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
}
|
|
2
|
+
import { safeStringify } from '../utils/serialize.js';
|
|
15
3
|
function toSchemaKey(schema) {
|
|
16
4
|
if (!schema || typeof schema !== 'object')
|
|
17
5
|
return `non_object:${typeof schema}`;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { normalizeToolResultReplacementState, } from '../session/replacement-state.js';
|
|
2
2
|
import { SUB_AGENT_CONTEXT_SNAPSHOT_VERSION, SUB_AGENT_CONTEXT_SNAPSHOT_FIELD_SEMANTICS, } from './types.js';
|
|
3
|
+
const SUPPORTED_SNAPSHOT_FIELDS = new Set([
|
|
4
|
+
'version',
|
|
5
|
+
...Object.keys(SUB_AGENT_CONTEXT_SNAPSHOT_FIELD_SEMANTICS),
|
|
6
|
+
]);
|
|
3
7
|
function deepClone(value) {
|
|
4
8
|
if (typeof structuredClone === 'function') {
|
|
5
9
|
return structuredClone(value);
|
|
@@ -39,7 +43,13 @@ function cloneConversationContext(messages) {
|
|
|
39
43
|
function cloneToolCallingAudit(entries) {
|
|
40
44
|
if (!Array.isArray(entries) || entries.length === 0)
|
|
41
45
|
return undefined;
|
|
42
|
-
return entries.map((entry) =>
|
|
46
|
+
return entries.map((entry) => ({
|
|
47
|
+
...entry,
|
|
48
|
+
toolResultPatchArtifact: cloneArtifactHandle(entry.toolResultPatchArtifact),
|
|
49
|
+
toolResultAuditArtifact: cloneArtifactHandle(entry.toolResultAuditArtifact),
|
|
50
|
+
toolResultReadArtifact: cloneArtifactHandle(entry.toolResultReadArtifact),
|
|
51
|
+
toolResultPreviewArtifact: cloneArtifactHandle(entry.toolResultPreviewArtifact),
|
|
52
|
+
}));
|
|
43
53
|
}
|
|
44
54
|
function cloneArtifactHints(hints) {
|
|
45
55
|
if (!hints)
|
|
@@ -106,11 +116,7 @@ function normalizeSnapshotVersion(snapshot) {
|
|
|
106
116
|
return version;
|
|
107
117
|
}
|
|
108
118
|
function assertSupportedSnapshotFields(snapshot) {
|
|
109
|
-
const
|
|
110
|
-
'version',
|
|
111
|
-
...Object.keys(SUB_AGENT_CONTEXT_SNAPSHOT_FIELD_SEMANTICS),
|
|
112
|
-
]);
|
|
113
|
-
const unknownFields = Object.keys(snapshot).filter((key) => !supportedFields.has(key));
|
|
119
|
+
const unknownFields = Object.keys(snapshot).filter((key) => !SUPPORTED_SNAPSHOT_FIELDS.has(key));
|
|
114
120
|
if (unknownFields.length > 0) {
|
|
115
121
|
throw new Error(`Unsupported sub-agent context snapshot fields: ${unknownFields.sort().join(', ')}`);
|
|
116
122
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
const LOG_HISTORY_LIMIT =
|
|
1
|
+
const LOG_HISTORY_LIMIT = 200;
|
|
2
2
|
export class InMemorySubAgentController {
|
|
3
3
|
agents = new Map();
|
|
4
|
+
toolCallListeners = new Set();
|
|
5
|
+
results = new Map();
|
|
6
|
+
waiters = new Map();
|
|
4
7
|
registerAgent(id, profile, status) {
|
|
5
8
|
const existing = this.agents.get(id);
|
|
6
9
|
if (existing) {
|
|
@@ -16,6 +19,8 @@ export class InMemorySubAgentController {
|
|
|
16
19
|
updatedAt: new Date(),
|
|
17
20
|
stopRequested: false,
|
|
18
21
|
logs: [],
|
|
22
|
+
tokenUsage: 0,
|
|
23
|
+
toolCallCount: 0,
|
|
19
24
|
});
|
|
20
25
|
}
|
|
21
26
|
updateStatus(id, status, summary) {
|
|
@@ -37,6 +42,35 @@ export class InMemorySubAgentController {
|
|
|
37
42
|
agent.logs.splice(0, agent.logs.length - LOG_HISTORY_LIMIT);
|
|
38
43
|
}
|
|
39
44
|
}
|
|
45
|
+
addTokenUsage(id, tokens) {
|
|
46
|
+
const agent = this.agents.get(id);
|
|
47
|
+
if (!agent)
|
|
48
|
+
return;
|
|
49
|
+
agent.tokenUsage += tokens;
|
|
50
|
+
}
|
|
51
|
+
recordToolCall(id, toolName, durationMs, success) {
|
|
52
|
+
const agent = this.agents.get(id);
|
|
53
|
+
if (!agent)
|
|
54
|
+
return;
|
|
55
|
+
agent.toolCallCount++;
|
|
56
|
+
const event = {
|
|
57
|
+
type: 'tool.call.end',
|
|
58
|
+
agentId: id,
|
|
59
|
+
toolName,
|
|
60
|
+
timestamp: Date.now(),
|
|
61
|
+
durationMs,
|
|
62
|
+
success,
|
|
63
|
+
};
|
|
64
|
+
for (const listener of this.toolCallListeners) {
|
|
65
|
+
listener(event);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
onToolCall(listener) {
|
|
69
|
+
this.toolCallListeners.add(listener);
|
|
70
|
+
return () => {
|
|
71
|
+
this.toolCallListeners.delete(listener);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
40
74
|
listAgents() {
|
|
41
75
|
return Array.from(this.agents.values());
|
|
42
76
|
}
|
|
@@ -62,6 +96,41 @@ export class InMemorySubAgentController {
|
|
|
62
96
|
isStopRequested(id) {
|
|
63
97
|
return this.agents.get(id)?.stopRequested ?? false;
|
|
64
98
|
}
|
|
99
|
+
setResult(id, result) {
|
|
100
|
+
this.results.set(id, result);
|
|
101
|
+
const waiters = this.waiters.get(id);
|
|
102
|
+
if (waiters) {
|
|
103
|
+
for (const resolve of waiters) {
|
|
104
|
+
resolve(result);
|
|
105
|
+
}
|
|
106
|
+
this.waiters.delete(id);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async awaitResult(id, timeoutMs = 300_000) {
|
|
110
|
+
// Check if result is already available
|
|
111
|
+
const existing = this.results.get(id);
|
|
112
|
+
if (existing)
|
|
113
|
+
return existing;
|
|
114
|
+
// Wait for the result with timeout
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
const timer = setTimeout(() => {
|
|
117
|
+
// Remove this waiter on timeout
|
|
118
|
+
const waiters = this.waiters.get(id);
|
|
119
|
+
if (waiters) {
|
|
120
|
+
const idx = waiters.indexOf(resolve);
|
|
121
|
+
if (idx >= 0)
|
|
122
|
+
waiters.splice(idx, 1);
|
|
123
|
+
}
|
|
124
|
+
resolve(undefined);
|
|
125
|
+
}, timeoutMs);
|
|
126
|
+
const waiters = this.waiters.get(id) ?? [];
|
|
127
|
+
waiters.push((result) => {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
resolve(result);
|
|
130
|
+
});
|
|
131
|
+
this.waiters.set(id, waiters);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
65
134
|
}
|
|
66
135
|
export function createSubAgentController() {
|
|
67
136
|
return new InMemorySubAgentController();
|
|
@@ -27,12 +27,34 @@ export class SmallfryLoop {
|
|
|
27
27
|
async execute(initCtx) {
|
|
28
28
|
getLogger().debug(`[SmallfryLoop] ${text.smallfry.status.working} (${this.profile.name})`);
|
|
29
29
|
let pipeline = Pipeline.of(initCtx);
|
|
30
|
+
let turnCount = 0;
|
|
31
|
+
const maxTurns = this.profile.maxTurns;
|
|
30
32
|
// Dynamic Phase Injection based on Stratagem
|
|
33
|
+
// PREFLIGHT is deterministic (no LLM call), so it doesn't count as a turn
|
|
31
34
|
pipeline = pipeline.step('PREFLIGHT', runPreflight);
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
// Each subsequent step makes at least one LLM call
|
|
36
|
+
if (maxTurns !== undefined && turnCount >= maxTurns) {
|
|
37
|
+
getLogger().warn(`[SmallfryLoop] maxTurns (${maxTurns}) reached before CONTEXT — stopping early`);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
pipeline = pipeline.step('CONTEXT', buildContext);
|
|
41
|
+
turnCount++;
|
|
42
|
+
}
|
|
43
|
+
if (maxTurns !== undefined && turnCount >= maxTurns) {
|
|
44
|
+
getLogger().warn(`[SmallfryLoop] maxTurns (${maxTurns}) reached before PLAN — stopping early`);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
pipeline = pipeline.step('PLAN', generatePlan);
|
|
48
|
+
turnCount++;
|
|
49
|
+
}
|
|
34
50
|
if (this.profile.stratagem === 'surgeon') {
|
|
35
|
-
|
|
51
|
+
if (maxTurns !== undefined && turnCount >= maxTurns) {
|
|
52
|
+
getLogger().warn(`[SmallfryLoop] maxTurns (${maxTurns}) reached before PATCH — stopping early`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
pipeline = pipeline.step('PATCH', generatePatch);
|
|
56
|
+
turnCount++;
|
|
57
|
+
}
|
|
36
58
|
}
|
|
37
59
|
const report = await pipeline.execute();
|
|
38
60
|
report.auditPath = await saveAudit(report, initCtx.options);
|