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
|
@@ -8,7 +8,9 @@ import { CanonicalResponsesEventEmitter, } from '../streaming/canonical/canonica
|
|
|
8
8
|
import { mapLlmStreamChunkToCanonicalStreamParts } from '../streaming/canonical/parts-from-llm-stream-chunk.js';
|
|
9
9
|
import { ArtifactStore } from '../sub-agent/artifacts/store.js';
|
|
10
10
|
import { Phase } from '../types/runtime.js';
|
|
11
|
+
import { extractNetworkCode, extractProvider, extractStatusCode } from '../utils/error.js';
|
|
11
12
|
import { isSafeRelativePath, normalizePath } from '../utils/path.js';
|
|
13
|
+
import { isRecord } from '../utils/serialize.js';
|
|
12
14
|
import { buildHeadlessToolInputPayload } from './headless-payload.js';
|
|
13
15
|
import { toolToOpenAI } from './mapper.js';
|
|
14
16
|
import { InMemoryLockManager } from './parallel/lock-manager.js';
|
|
@@ -17,6 +19,13 @@ import { ParallelScheduler } from './parallel/scheduler.js';
|
|
|
17
19
|
import { isRecoverableToolInputErrorCode } from './recoverable-tool-errors.js';
|
|
18
20
|
import { ToolCallAccumulator } from './streaming/ToolCallAccumulator.js';
|
|
19
21
|
import { resolveVisibleToolSpecs } from './tool-visibility.js';
|
|
22
|
+
function findLast(array, predicate) {
|
|
23
|
+
for (let i = array.length - 1; i >= 0; i--) {
|
|
24
|
+
if (predicate(array[i]))
|
|
25
|
+
return array[i];
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
20
29
|
function safeParseJson(argsText) {
|
|
21
30
|
if (typeof argsText !== 'string') {
|
|
22
31
|
return { ok: true, value: argsText };
|
|
@@ -126,7 +135,7 @@ function isArtifactHandleRecord(value) {
|
|
|
126
135
|
typeof candidate.size === 'number');
|
|
127
136
|
}
|
|
128
137
|
function extractArtifactHandlesFromToolOutput(output) {
|
|
129
|
-
if (!
|
|
138
|
+
if (!isRecord(output)) {
|
|
130
139
|
return {};
|
|
131
140
|
}
|
|
132
141
|
const patchArtifact = isArtifactHandleRecord(output.patchArtifact)
|
|
@@ -144,12 +153,12 @@ function extractRecentReadResult(params) {
|
|
|
144
153
|
if (params.toolName !== 'fs.read' && params.toolName !== 'code.read') {
|
|
145
154
|
return undefined;
|
|
146
155
|
}
|
|
147
|
-
if (!
|
|
156
|
+
if (!isRecord(params.output) || typeof params.output.content !== 'string') {
|
|
148
157
|
return undefined;
|
|
149
158
|
}
|
|
150
159
|
const args = safeParseJson(params.rawArgs);
|
|
151
160
|
const argsValue = args.ok ? args.value : params.rawArgs;
|
|
152
|
-
if (!
|
|
161
|
+
if (!isRecord(argsValue))
|
|
153
162
|
return undefined;
|
|
154
163
|
const file = argsValue.file ?? argsValue.file_path ?? argsValue.filePath ?? argsValue.path;
|
|
155
164
|
if (typeof file !== 'string' || !file.trim())
|
|
@@ -703,7 +712,7 @@ export async function chatWithTools(initialMessages, chatOptions, session) {
|
|
|
703
712
|
tool_calls: assistant.tool_calls,
|
|
704
713
|
});
|
|
705
714
|
const toolCalls = assistant.tool_calls || [];
|
|
706
|
-
if (
|
|
715
|
+
if (toolCalls.length === 0) {
|
|
707
716
|
if (session.llmOutput) {
|
|
708
717
|
emitLlmOutput({
|
|
709
718
|
emit: session.emit,
|
|
@@ -728,7 +737,7 @@ export async function chatWithTools(initialMessages, chatOptions, session) {
|
|
|
728
737
|
message: `Tool calling exceeded maximum rounds (${maxRounds}); continuing without further tool execution`,
|
|
729
738
|
timestamp: new Date(),
|
|
730
739
|
});
|
|
731
|
-
const lastAssistant =
|
|
740
|
+
const lastAssistant = findLast(messages, (m) => m.role === 'assistant');
|
|
732
741
|
if (session.llmOutput && lastAssistant?.content) {
|
|
733
742
|
emitLlmOutput({
|
|
734
743
|
emit: session.emit,
|
|
@@ -740,11 +749,8 @@ export async function chatWithTools(initialMessages, chatOptions, session) {
|
|
|
740
749
|
}
|
|
741
750
|
return lastAssistant || { role: 'assistant', content: '' };
|
|
742
751
|
}
|
|
743
|
-
function isObjectRecord(value) {
|
|
744
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
745
|
-
}
|
|
746
752
|
function isPlainObject(value) {
|
|
747
|
-
if (!
|
|
753
|
+
if (!isRecord(value))
|
|
748
754
|
return false;
|
|
749
755
|
const proto = Object.getPrototypeOf(value);
|
|
750
756
|
return proto === Object.prototype || proto === null;
|
|
@@ -789,61 +795,6 @@ function coercePlanUpdatePatch(args) {
|
|
|
789
795
|
}
|
|
790
796
|
return { args, error: formatPlanUpdatePatchTypeError(describeValueType(patch)) };
|
|
791
797
|
}
|
|
792
|
-
function unwrapRetryError(err) {
|
|
793
|
-
if (!err || typeof err !== 'object')
|
|
794
|
-
return err;
|
|
795
|
-
const candidate = err;
|
|
796
|
-
if (candidate.lastError)
|
|
797
|
-
return candidate.lastError;
|
|
798
|
-
return err;
|
|
799
|
-
}
|
|
800
|
-
function extractStatusCode(err) {
|
|
801
|
-
const unwrapped = unwrapRetryError(err);
|
|
802
|
-
if (!unwrapped || typeof unwrapped !== 'object')
|
|
803
|
-
return undefined;
|
|
804
|
-
const meta = unwrapped?.meta;
|
|
805
|
-
if (meta && typeof meta === 'object' && typeof meta.statusCode === 'number') {
|
|
806
|
-
return meta.statusCode;
|
|
807
|
-
}
|
|
808
|
-
const statusCode = unwrapped?.statusCode;
|
|
809
|
-
if (typeof statusCode === 'number')
|
|
810
|
-
return statusCode;
|
|
811
|
-
const response = unwrapped?.response;
|
|
812
|
-
if (response && typeof response === 'object' && typeof response.status === 'number') {
|
|
813
|
-
return response.status;
|
|
814
|
-
}
|
|
815
|
-
return undefined;
|
|
816
|
-
}
|
|
817
|
-
function extractNetworkCode(err) {
|
|
818
|
-
const unwrapped = unwrapRetryError(err);
|
|
819
|
-
if (!unwrapped || typeof unwrapped !== 'object')
|
|
820
|
-
return undefined;
|
|
821
|
-
const code = unwrapped?.code;
|
|
822
|
-
if (typeof code === 'string')
|
|
823
|
-
return code;
|
|
824
|
-
const cause = unwrapped?.cause;
|
|
825
|
-
if (cause && typeof cause === 'object' && typeof cause.code === 'string') {
|
|
826
|
-
return cause.code;
|
|
827
|
-
}
|
|
828
|
-
const meta = unwrapped?.meta;
|
|
829
|
-
if (meta && typeof meta === 'object' && typeof meta.causeName === 'string') {
|
|
830
|
-
return meta.causeName;
|
|
831
|
-
}
|
|
832
|
-
return undefined;
|
|
833
|
-
}
|
|
834
|
-
function extractProvider(err) {
|
|
835
|
-
const unwrapped = unwrapRetryError(err);
|
|
836
|
-
if (!unwrapped || typeof unwrapped !== 'object')
|
|
837
|
-
return undefined;
|
|
838
|
-
const meta = unwrapped?.meta;
|
|
839
|
-
if (meta && typeof meta === 'object' && typeof meta.provider === 'string') {
|
|
840
|
-
return meta.provider;
|
|
841
|
-
}
|
|
842
|
-
const provider = unwrapped?.provider;
|
|
843
|
-
if (typeof provider === 'string')
|
|
844
|
-
return provider;
|
|
845
|
-
return undefined;
|
|
846
|
-
}
|
|
847
798
|
const ENABLE_TOOL_ARG_REPAIR = process.env.SALMONLOOP_ENABLE_TOOL_ARG_REPAIR === '1' ||
|
|
848
799
|
process.env.SALMONLOOP_ENABLE_TOOL_ARG_REPAIR === 'true';
|
|
849
800
|
const SAFE_INFERRED_EXTENSIONS = new Set([
|
|
@@ -901,7 +852,7 @@ function inferHighConfidenceFiles(instruction) {
|
|
|
901
852
|
return Array.from(new Set(candidates));
|
|
902
853
|
}
|
|
903
854
|
function extractInstructionText(messages) {
|
|
904
|
-
const lastUser =
|
|
855
|
+
const lastUser = findLast(messages, (m) => m.role === 'user');
|
|
905
856
|
const text = typeof lastUser?.content === 'string' ? lastUser.content : '';
|
|
906
857
|
if (!text)
|
|
907
858
|
return '';
|
|
@@ -921,7 +872,7 @@ function prepareToolCallRequests(calls) {
|
|
|
921
872
|
async function runToolExecutionPlan(params) {
|
|
922
873
|
const scheduler = new ParallelScheduler(params.session.toolstack.router, new InMemoryLockManager());
|
|
923
874
|
const runSignal = params.signal ?? new AbortController().signal;
|
|
924
|
-
let result =
|
|
875
|
+
let result = await scheduler.run(params.plan, { ...params.session.runtime, phase: params.phase }, runSignal);
|
|
925
876
|
const persistEnabled = process.env.NODE_ENV !== 'test';
|
|
926
877
|
const persistenceRoot = params.session.runtime.persistenceRoot || params.session.runtime.repoRoot;
|
|
927
878
|
if (persistEnabled) {
|
|
@@ -943,10 +894,10 @@ async function runToolExecutionPlan(params) {
|
|
|
943
894
|
await Promise.all(result.blockedApprovals.map(async (a) => {
|
|
944
895
|
await waitForAuthorization?.(a.nodeId, runSignal);
|
|
945
896
|
}));
|
|
946
|
-
result =
|
|
897
|
+
result = await scheduler.run(params.plan, { ...params.session.runtime, phase: params.phase }, runSignal, {
|
|
947
898
|
initialResults: result.nodeResults,
|
|
948
899
|
resumeBlockedApprovals: true,
|
|
949
|
-
})
|
|
900
|
+
});
|
|
950
901
|
if (persistEnabled) {
|
|
951
902
|
await PlanPersistence.save(persistenceRoot, params.plan, result, {
|
|
952
903
|
repoRoot: params.session.runtime.repoRoot,
|
|
@@ -983,12 +934,14 @@ function applyStrictToolOutputSchemaValidation(params) {
|
|
|
983
934
|
}
|
|
984
935
|
async function executeToolCalls(session, phase, round, calls, messages, signal) {
|
|
985
936
|
const prepared = prepareToolCallRequests(calls);
|
|
937
|
+
const specByName = new Map();
|
|
938
|
+
for (const spec of session.toolstack.registry.listAll()) {
|
|
939
|
+
specByName.set(spec.name, spec);
|
|
940
|
+
}
|
|
986
941
|
const bucketByCallId = new Map();
|
|
987
942
|
const preparedCounts = { regular: 0, agent: 0 };
|
|
988
943
|
for (const item of prepared) {
|
|
989
|
-
const spec = typeof item.toolName === 'string'
|
|
990
|
-
? session.toolstack.registry.listAll().find((s) => s.name === item.toolName)
|
|
991
|
-
: undefined;
|
|
944
|
+
const spec = typeof item.toolName === 'string' ? specByName.get(item.toolName) : undefined;
|
|
992
945
|
const bucket = spec?.intent === 'AGENT' ? 'agent' : 'regular';
|
|
993
946
|
bucketByCallId.set(item.callId, bucket);
|
|
994
947
|
preparedCounts[bucket]++;
|
|
@@ -1031,7 +984,7 @@ async function executeToolCalls(session, phase, round, calls, messages, signal)
|
|
|
1031
984
|
ENABLE_TOOL_ARG_REPAIR &&
|
|
1032
985
|
phase === Phase.EXPLORE &&
|
|
1033
986
|
normalizedToolName === 'fs.read' &&
|
|
1034
|
-
|
|
987
|
+
isRecord(argsValue) &&
|
|
1035
988
|
typeof argsValue.file !== 'string') {
|
|
1036
989
|
const instruction = extractInstructionText(messages);
|
|
1037
990
|
const inferred = inferHighConfidenceFiles(instruction);
|
|
@@ -1040,7 +993,7 @@ async function executeToolCalls(session, phase, round, calls, messages, signal)
|
|
|
1040
993
|
}
|
|
1041
994
|
}
|
|
1042
995
|
let planUpdatePatchError;
|
|
1043
|
-
if (parsedArgsOk && normalizedToolName === 'plan.update' &&
|
|
996
|
+
if (parsedArgsOk && normalizedToolName === 'plan.update' && isRecord(argsValue)) {
|
|
1044
997
|
const patchGuard = coercePlanUpdatePatch(argsValue);
|
|
1045
998
|
argsValue = patchGuard.args;
|
|
1046
999
|
if (patchGuard.coercedPatchSource) {
|
|
@@ -1056,9 +1009,7 @@ async function executeToolCalls(session, phase, round, calls, messages, signal)
|
|
|
1056
1009
|
const input = session.eventPayload?.includeToolInput && parsedArgsOk
|
|
1057
1010
|
? buildHeadlessToolInputPayload(argsValue)
|
|
1058
1011
|
: undefined;
|
|
1059
|
-
const spec = typeof toolName === 'string'
|
|
1060
|
-
? session.toolstack.registry.listAll().find((s) => s.name === toolName)
|
|
1061
|
-
: undefined;
|
|
1012
|
+
const spec = typeof toolName === 'string' ? specByName.get(toolName) : undefined;
|
|
1062
1013
|
if (typeof toolName === 'string') {
|
|
1063
1014
|
session.emit?.({
|
|
1064
1015
|
type: 'log',
|
|
@@ -1177,7 +1128,8 @@ async function executeToolCalls(session, phase, round, calls, messages, signal)
|
|
|
1177
1128
|
};
|
|
1178
1129
|
const patchCoercionSource = patchCoercionByCallId.get(callId);
|
|
1179
1130
|
if (patchCoercionSource) {
|
|
1180
|
-
parsedAuditEntry.coercedPatchSource =
|
|
1131
|
+
parsedAuditEntry.coercedPatchSource =
|
|
1132
|
+
patchCoercionSource;
|
|
1181
1133
|
}
|
|
1182
1134
|
session.toolCallingAudit?.event(parsedAuditEntry);
|
|
1183
1135
|
if (planUpdatePatchError) {
|
|
@@ -1269,9 +1221,10 @@ async function executeToolCalls(session, phase, round, calls, messages, signal)
|
|
|
1269
1221
|
if (result.status !== 'ok' &&
|
|
1270
1222
|
result.error?.code === 'INTERRUPT_REQUIRED' &&
|
|
1271
1223
|
result.meta?.interrupt) {
|
|
1272
|
-
const err = new Error(result.error.message || 'Interrupt required')
|
|
1273
|
-
|
|
1274
|
-
|
|
1224
|
+
const err = Object.assign(new Error(result.error.message || 'Interrupt required'), {
|
|
1225
|
+
code: 'INTERRUPT_REQUIRED',
|
|
1226
|
+
interrupt: result.meta.interrupt,
|
|
1227
|
+
});
|
|
1275
1228
|
throw err;
|
|
1276
1229
|
}
|
|
1277
1230
|
if (result.status !== 'ok') {
|
|
@@ -1295,26 +1248,30 @@ async function executeToolCalls(session, phase, round, calls, messages, signal)
|
|
|
1295
1248
|
};
|
|
1296
1249
|
const patchCoercionSource = patchCoercionByCallId.get(callId);
|
|
1297
1250
|
if (patchCoercionSource) {
|
|
1298
|
-
errorAuditEntry.coercedPatchSource =
|
|
1251
|
+
errorAuditEntry.coercedPatchSource =
|
|
1252
|
+
patchCoercionSource;
|
|
1299
1253
|
}
|
|
1300
1254
|
session.toolCallingAudit?.event(errorAuditEntry);
|
|
1301
1255
|
}
|
|
1302
1256
|
else {
|
|
1303
|
-
const toolResultOutputOk =
|
|
1257
|
+
const toolResultOutputOk = isRecord(result.output) && typeof result.output.ok === 'boolean'
|
|
1304
1258
|
? result.output.ok
|
|
1305
1259
|
: undefined;
|
|
1306
1260
|
const artifacts = extractArtifactHandlesFromToolOutput(result.output);
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1261
|
+
const resolvedToolName = typeof toolName === 'string' ? toolName : 'unknown';
|
|
1262
|
+
const [recentReadArtifact, toolResultPreviewArtifact] = await Promise.all([
|
|
1263
|
+
persistRecentReadArtifact({
|
|
1264
|
+
toolName: resolvedToolName,
|
|
1265
|
+
rawArgs,
|
|
1266
|
+
output: result.output,
|
|
1267
|
+
}),
|
|
1268
|
+
persistToolResultPreviewArtifact({
|
|
1269
|
+
toolName: resolvedToolName,
|
|
1270
|
+
output: result.output,
|
|
1271
|
+
summary: result.summary,
|
|
1272
|
+
outputSummary: result.outputSummary,
|
|
1273
|
+
}),
|
|
1274
|
+
]);
|
|
1318
1275
|
session.toolCallingAudit?.event({
|
|
1319
1276
|
timestamp: new Date().toISOString(),
|
|
1320
1277
|
phase,
|
|
@@ -1410,9 +1367,9 @@ export async function chatWithToolsStreaming(initialMessages, chatOptions, sessi
|
|
|
1410
1367
|
statusCode: extractStatusCode(e),
|
|
1411
1368
|
networkCode: extractNetworkCode(e),
|
|
1412
1369
|
errorName: e instanceof Error ? e.name : 'UnknownError',
|
|
1413
|
-
errorCode: typeof e
|
|
1370
|
+
errorCode: isRecord(e) && typeof e.llmCode === 'string'
|
|
1414
1371
|
? e.llmCode
|
|
1415
|
-
: typeof e
|
|
1372
|
+
: isRecord(e) && typeof e.code === 'string'
|
|
1416
1373
|
? e.code
|
|
1417
1374
|
: undefined,
|
|
1418
1375
|
}, { source: 'llm', severity: 'low', scope: 'session', phase });
|
|
@@ -1470,7 +1427,7 @@ export async function chatWithToolsStreaming(initialMessages, chatOptions, sessi
|
|
|
1470
1427
|
}, { source: 'llm', severity: 'low', scope: 'session', phase });
|
|
1471
1428
|
messages.push(assistant);
|
|
1472
1429
|
const calls = assistant.tool_calls || [];
|
|
1473
|
-
if (
|
|
1430
|
+
if (calls.length === 0) {
|
|
1474
1431
|
if (session.llmOutput) {
|
|
1475
1432
|
emitLlmStreamEnd({
|
|
1476
1433
|
emit: session.emit,
|
|
@@ -1509,7 +1466,7 @@ export async function chatWithToolsStreaming(initialMessages, chatOptions, sessi
|
|
|
1509
1466
|
message: `Tool calling exceeded maximum rounds (${maxRounds}); continuing without further tool execution`,
|
|
1510
1467
|
timestamp: new Date(),
|
|
1511
1468
|
});
|
|
1512
|
-
const lastAssistant =
|
|
1469
|
+
const lastAssistant = findLast(messages, (m) => m.role === 'assistant');
|
|
1513
1470
|
return lastAssistant || { role: 'assistant', content: '' };
|
|
1514
1471
|
}
|
|
1515
1472
|
//# sourceMappingURL=session.js.map
|
|
@@ -46,7 +46,8 @@ export function resolvePhaseVisibleTools(params) {
|
|
|
46
46
|
export function resolveVisibleToolSpecs(params) {
|
|
47
47
|
if (!params.toolstack)
|
|
48
48
|
return [];
|
|
49
|
-
const
|
|
49
|
+
const { toolstack } = params;
|
|
50
|
+
const allowedSpecs = toolstack.registry.listAll().filter((spec) => toolstack.policy.decide(params.phase, spec, {
|
|
50
51
|
worktreeRoot: params.worktreeRoot,
|
|
51
52
|
flowMode: params.flowMode,
|
|
52
53
|
}).allowed);
|
package/dist/core/tools/types.js
CHANGED
|
@@ -7,4 +7,14 @@ export const TOOL_INTENTS = [
|
|
|
7
7
|
'AGENT',
|
|
8
8
|
'REPORT',
|
|
9
9
|
];
|
|
10
|
+
/**
|
|
11
|
+
* Type-safe helper to create a fully-formed ToolSpec from a spec definition and executor.
|
|
12
|
+
* Eliminates the `{ ...spec, executor: exec as any }` boilerplate pattern.
|
|
13
|
+
*
|
|
14
|
+
* The executor parameter accepts a superset of ToolRuntimeCtx (e.g., with phase narrowed
|
|
15
|
+
* to non-optional) to support tools that require the router-injected phase field.
|
|
16
|
+
*/
|
|
17
|
+
export function defineTool(spec, executor) {
|
|
18
|
+
return { ...spec, executor: executor };
|
|
19
|
+
}
|
|
10
20
|
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { isRecord } from './serialize.js';
|
|
2
|
+
/**
|
|
3
|
+
* Extract a human-readable error message from an unknown thrown value.
|
|
4
|
+
* Handles Error instances, strings, and falls back to String(value).
|
|
5
|
+
*/
|
|
6
|
+
export function errorMessage(error) {
|
|
7
|
+
if (error instanceof Error)
|
|
8
|
+
return error.message;
|
|
9
|
+
if (typeof error === 'string')
|
|
10
|
+
return error;
|
|
11
|
+
return String(error);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Unwrap retry-style errors that wrap the real error in a `lastError` property.
|
|
15
|
+
*/
|
|
16
|
+
export function unwrapRetryError(err) {
|
|
17
|
+
if (!isRecord(err))
|
|
18
|
+
return err;
|
|
19
|
+
if (err.lastError)
|
|
20
|
+
return err.lastError;
|
|
21
|
+
return err;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Extract an HTTP-style status code from an error object.
|
|
25
|
+
* Checks: meta.statusCode, statusCode, response.status
|
|
26
|
+
*/
|
|
27
|
+
export function extractStatusCode(err) {
|
|
28
|
+
const unwrapped = unwrapRetryError(err);
|
|
29
|
+
if (!isRecord(unwrapped))
|
|
30
|
+
return undefined;
|
|
31
|
+
const meta = unwrapped.meta;
|
|
32
|
+
if (isRecord(meta) && typeof meta.statusCode === 'number') {
|
|
33
|
+
return meta.statusCode;
|
|
34
|
+
}
|
|
35
|
+
if (typeof unwrapped.statusCode === 'number')
|
|
36
|
+
return unwrapped.statusCode;
|
|
37
|
+
const response = unwrapped.response;
|
|
38
|
+
if (isRecord(response) && typeof response.status === 'number') {
|
|
39
|
+
return response.status;
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Extract a network error code from an error object.
|
|
45
|
+
* Checks: code, cause.code, meta.causeName
|
|
46
|
+
*/
|
|
47
|
+
export function extractNetworkCode(err) {
|
|
48
|
+
const unwrapped = unwrapRetryError(err);
|
|
49
|
+
if (!isRecord(unwrapped))
|
|
50
|
+
return undefined;
|
|
51
|
+
if (typeof unwrapped.code === 'string')
|
|
52
|
+
return unwrapped.code;
|
|
53
|
+
const cause = unwrapped.cause;
|
|
54
|
+
if (isRecord(cause) && typeof cause.code === 'string') {
|
|
55
|
+
return cause.code;
|
|
56
|
+
}
|
|
57
|
+
const meta = unwrapped.meta;
|
|
58
|
+
if (isRecord(meta) && typeof meta.causeName === 'string') {
|
|
59
|
+
return meta.causeName;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Extract a provider name from an error object.
|
|
65
|
+
* Checks: meta.provider, provider
|
|
66
|
+
*/
|
|
67
|
+
export function extractProvider(err) {
|
|
68
|
+
const unwrapped = unwrapRetryError(err);
|
|
69
|
+
if (!isRecord(unwrapped))
|
|
70
|
+
return undefined;
|
|
71
|
+
const meta = unwrapped.meta;
|
|
72
|
+
if (isRecord(meta) && typeof meta.provider === 'string') {
|
|
73
|
+
return meta.provider;
|
|
74
|
+
}
|
|
75
|
+
if (typeof unwrapped.provider === 'string')
|
|
76
|
+
return unwrapped.provider;
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=error.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safely serialize a value to JSON string.
|
|
3
|
+
* Returns '[Unserializable]' if JSON.stringify throws and String() also fails.
|
|
4
|
+
*/
|
|
5
|
+
export function safeStringify(value, options) {
|
|
6
|
+
try {
|
|
7
|
+
const raw = JSON.stringify(value, null, options?.indent);
|
|
8
|
+
if (options?.maxLength && raw.length > options.maxLength) {
|
|
9
|
+
return `${raw.slice(0, options.maxLength)}...`;
|
|
10
|
+
}
|
|
11
|
+
return raw;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
try {
|
|
15
|
+
return String(value);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return '[Unserializable]';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Type guard: narrow an unknown value to Record<string, unknown>.
|
|
24
|
+
* Returns false for arrays, primitives, null, and undefined.
|
|
25
|
+
*/
|
|
26
|
+
export function isRecord(value) {
|
|
27
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Narrow an unknown value to a Record<string, unknown>.
|
|
31
|
+
* Returns an empty object for non-object inputs (arrays, primitives, null, undefined).
|
|
32
|
+
*/
|
|
33
|
+
export function asRecord(value) {
|
|
34
|
+
return isRecord(value) ? value : {};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Extract a string value from a record by key.
|
|
38
|
+
* Returns null if the key doesn't exist or the value isn't a string.
|
|
39
|
+
*/
|
|
40
|
+
export function getString(record, key) {
|
|
41
|
+
const value = record[key];
|
|
42
|
+
return typeof value === 'string' ? value : null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Extract a nested record from a record by key.
|
|
46
|
+
* Returns null if the key doesn't exist or the value isn't a record.
|
|
47
|
+
*/
|
|
48
|
+
export function getRecord(record, key) {
|
|
49
|
+
const value = record[key];
|
|
50
|
+
return isRecord(value) ? value : null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Extract a human-readable error message from an unknown thrown value.
|
|
54
|
+
* Handles Error instances, strings, and falls back to String(value).
|
|
55
|
+
*/
|
|
56
|
+
export function errorMessage(error) {
|
|
57
|
+
if (error instanceof Error)
|
|
58
|
+
return error.message;
|
|
59
|
+
if (typeof error === 'string')
|
|
60
|
+
return error;
|
|
61
|
+
return String(error);
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=serialize.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Unwrap Zod wrapper types (ZodPipe, ZodOptional, ZodNullable, ZodDefault)
|
|
4
|
+
* to get the underlying schema. Useful for schema generation and hint building.
|
|
5
|
+
*/
|
|
6
|
+
export function unwrapZodSchema(schema) {
|
|
7
|
+
let current = schema;
|
|
8
|
+
for (let depth = 0; depth < 20; depth++) {
|
|
9
|
+
if (current instanceof z.ZodPipe) {
|
|
10
|
+
current = current.def.out;
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (current instanceof z.ZodOptional) {
|
|
14
|
+
current = current.def.innerType;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (current instanceof z.ZodNullable) {
|
|
18
|
+
current = current.def.innerType;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (current instanceof z.ZodDefault) {
|
|
22
|
+
current = current.def.innerType;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
return current;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=zod.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { access, constants } from '../adapters/fs/node-fs.js';
|
|
2
2
|
import { GitAdapter } from '../adapters/git/git-adapter.js';
|
|
3
3
|
import { LIMITS } from '../config/limits.js';
|
|
4
|
+
import { errorMessage } from '../utils/error.js';
|
|
4
5
|
const PROBE_LIMITS = { maxStdoutBytes: 4_096, maxStderrChars: 4_096 };
|
|
5
6
|
function gitFailureReason(result) {
|
|
6
7
|
if (result.error?.code === 'ENOENT')
|
|
@@ -23,7 +24,7 @@ async function detectFileSystemCapability(workspacePath) {
|
|
|
23
24
|
return {
|
|
24
25
|
readable: false,
|
|
25
26
|
writable: false,
|
|
26
|
-
reason:
|
|
27
|
+
reason: errorMessage(error),
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
30
|
try {
|
|
@@ -34,7 +35,7 @@ async function detectFileSystemCapability(workspacePath) {
|
|
|
34
35
|
return {
|
|
35
36
|
readable: true,
|
|
36
37
|
writable: false,
|
|
37
|
-
reason:
|
|
38
|
+
reason: errorMessage(error),
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile } from '../../core/adapters/fs/node-fs.js';
|
|
2
2
|
import { recordAuditEvent } from '../../core/observability/audit-trail.js';
|
|
3
3
|
import { getLogger } from '../../core/observability/logger.js';
|
|
4
|
+
import { isRecord } from '../../core/utils/serialize.js';
|
|
4
5
|
import { text } from '../../locales/index.js';
|
|
5
6
|
function nowIso() {
|
|
6
7
|
return new Date().toISOString();
|
|
@@ -43,7 +44,7 @@ function buildPhaseDurations(traces) {
|
|
|
43
44
|
return undefined;
|
|
44
45
|
const out = {};
|
|
45
46
|
for (const t of traces) {
|
|
46
|
-
if (!t
|
|
47
|
+
if (!isRecord(t))
|
|
47
48
|
continue;
|
|
48
49
|
const name = t.name;
|
|
49
50
|
const duration = t.duration;
|
|
@@ -69,13 +70,13 @@ async function tryReadAuditJson(auditPath) {
|
|
|
69
70
|
}
|
|
70
71
|
function extractNetworkErrorCode(error) {
|
|
71
72
|
const allow = (value) => typeof value === 'string' && /^[A-Z0-9_]{2,32}$/.test(value) ? value : undefined;
|
|
72
|
-
if (error
|
|
73
|
-
const
|
|
74
|
-
return (allow(
|
|
75
|
-
allow(
|
|
76
|
-
allow(
|
|
77
|
-
allow(
|
|
78
|
-
allow(
|
|
73
|
+
if (isRecord(error)) {
|
|
74
|
+
const cause = isRecord(error.cause) ? error.cause : undefined;
|
|
75
|
+
return (allow(error.code) ||
|
|
76
|
+
allow(cause?.code) ||
|
|
77
|
+
allow(cause?.errno) ||
|
|
78
|
+
allow(error.errno) ||
|
|
79
|
+
allow(cause?.name));
|
|
79
80
|
}
|
|
80
81
|
return undefined;
|
|
81
82
|
}
|
package/dist/locales/en.js
CHANGED
|
@@ -770,7 +770,8 @@ Please return the patch in PURE unified diff format:`;
|
|
|
770
770
|
missionFailedWithReason: (reason) => `Smallfry mission failed: ${reason}`,
|
|
771
771
|
},
|
|
772
772
|
ui: {
|
|
773
|
-
spawnToolDescription: 'Delegate a concrete sub-task to a specialized sub-agent. This is not a no-argument action: always provide agent_ref and task. Use agent_ref="explorer" for read-only investigation, "reviewer" for audit, "surgeon" for implementation proposals, or "cleaner" for lint/format cleanup. Keep task self-contained with relevant files and the exact deliverable. Omit session_target unless shared context is explicitly required.',
|
|
773
|
+
spawnToolDescription: 'Delegate a concrete sub-task to a specialized sub-agent. This is not a no-argument action: always provide agent_ref and task. Use agent_ref="explorer" for read-only investigation, "reviewer" for audit, "surgeon" for implementation proposals, or "cleaner" for lint/format cleanup. Keep task self-contained with relevant files and the exact deliverable. Omit session_target unless shared context is explicitly required. Set async=true to get a handle immediately and use agent_await to collect the result later.',
|
|
774
|
+
awaitToolDescription: 'Wait for an async sub-agent (spawned with agent_dispatch async=true) to complete and return its result. Pass the agentId from the dispatch handle.',
|
|
774
775
|
progressTitle: (id) => `[Smallfry: ${id}]`,
|
|
775
776
|
},
|
|
776
777
|
},
|