salmon-loop 0.3.1 → 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.
Files changed (123) hide show
  1. package/dist/cli/authorization/non-interactive.js +9 -13
  2. package/dist/cli/chat.js +12 -6
  3. package/dist/cli/commands/allowlist.js +1 -1
  4. package/dist/cli/commands/chat.js +13 -13
  5. package/dist/cli/commands/parallel.js +1 -1
  6. package/dist/cli/commands/run/handler.js +6 -3
  7. package/dist/cli/commands/run/loop-params.js +1 -0
  8. package/dist/cli/commands/run/parse-options.js +14 -26
  9. package/dist/cli/commands/run/runtime-llm.js +15 -12
  10. package/dist/cli/commands/serve.js +14 -1
  11. package/dist/cli/headless/openai-responses-canonical-applier.js +1 -7
  12. package/dist/cli/reporters/standard.js +2 -3
  13. package/dist/cli/reporters/stream-json.js +2 -1
  14. package/dist/cli/slash/runtime.js +2 -2
  15. package/dist/cli/ui/components/CommandSuggestionList.js +1 -1
  16. package/dist/cli/ui/hooks/useLoopEvents.js +1 -1
  17. package/dist/cli/ui/hooks/useLoopState.js +1 -1
  18. package/dist/core/ast/parser.js +18 -9
  19. package/dist/core/config/schema.js +738 -0
  20. package/dist/core/config/validate.js +11 -922
  21. package/dist/core/context/gatherers/ast-gatherer.js +4 -12
  22. package/dist/core/context/gatherers/ghost-dependency-gatherer.js +0 -1
  23. package/dist/core/context/gatherers/knowledge-gatherer.js +3 -0
  24. package/dist/core/context/service.js +39 -8
  25. package/dist/core/context/token/encoding-registry.js +7 -6
  26. package/dist/core/extensions/index.js +48 -3
  27. package/dist/core/extensions/load.js +3 -2
  28. package/dist/core/extensions/merge.js +5 -1
  29. package/dist/core/extensions/paths.js +6 -0
  30. package/dist/core/extensions/schemas.js +21 -0
  31. package/dist/core/facades/cli-command-chat.js +2 -0
  32. package/dist/core/facades/cli-run-handler.js +1 -0
  33. package/dist/core/facades/cli-utils-serialize.js +2 -0
  34. package/dist/core/grizzco/dsl/llm-strategy.js +3 -2
  35. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +15 -10
  36. package/dist/core/grizzco/engine/pipeline/pipeline.js +149 -240
  37. package/dist/core/grizzco/engine/transaction/attempt-failure.js +5 -4
  38. package/dist/core/grizzco/engine/transaction/authorization-summary.js +2 -1
  39. package/dist/core/grizzco/runtime/apply-back-runtime.js +2 -1
  40. package/dist/core/grizzco/services/registry.js +18 -0
  41. package/dist/core/grizzco/steps/audit.js +20 -10
  42. package/dist/core/grizzco/steps/display-report.js +4 -11
  43. package/dist/core/grizzco/steps/explore.js +9 -2
  44. package/dist/core/grizzco/steps/patch/prompt-input.js +4 -1
  45. package/dist/core/grizzco/steps/patch.js +1 -0
  46. package/dist/core/grizzco/steps/plan.js +58 -49
  47. package/dist/core/grizzco/steps/tool-runtime.js +3 -0
  48. package/dist/core/grizzco/workers/strata-sync-worker.js +2 -1
  49. package/dist/core/llm/ai-sdk/message-mapper.js +24 -18
  50. package/dist/core/llm/ai-sdk/request-params.js +1 -3
  51. package/dist/core/llm/ai-sdk/result-mapper.js +14 -8
  52. package/dist/core/llm/ai-sdk/retry-classifier.js +6 -4
  53. package/dist/core/llm/contracts/repair.js +16 -8
  54. package/dist/core/llm/errors.js +13 -10
  55. package/dist/core/llm/output-policy.js +8 -0
  56. package/dist/core/llm/redact.js +1 -3
  57. package/dist/core/llm/sub-agent-factory.js +48 -0
  58. package/dist/core/llm/tool-calling-stub.js +48 -0
  59. package/dist/core/llm/utils.js +17 -6
  60. package/dist/core/mcp/bridge/prompt-command-provider.js +4 -3
  61. package/dist/core/mcp/bridge/tool-bridge.js +5 -14
  62. package/dist/core/mcp/client/connection-manager.js +3 -2
  63. package/dist/core/mcp/host/sampling-provider.js +1 -1
  64. package/dist/core/mcp/schema/json-schema-to-zod.js +2 -1
  65. package/dist/core/memory/relevant-retrieval.js +6 -4
  66. package/dist/core/observability/authorization-decisions.js +13 -12
  67. package/dist/core/observability/error-mapping.js +2 -1
  68. package/dist/core/observability/token-usage.js +5 -4
  69. package/dist/core/plugin/loader.js +5 -4
  70. package/dist/core/prompts/registry.js +11 -29
  71. package/dist/core/protocols/a2a/sdk/server.js +2 -3
  72. package/dist/core/protocols/acp/formal-agent.js +10 -4
  73. package/dist/core/protocols/acp/stdio-server.js +6 -6
  74. package/dist/core/runtime/agent-server-runtime.js +3 -2
  75. package/dist/core/runtime/initialize.js +70 -6
  76. package/dist/core/session/compaction/index.js +4 -3
  77. package/dist/core/session/manager.js +41 -47
  78. package/dist/core/session/token-tracker.js +18 -7
  79. package/dist/core/skills/parser.js +3 -2
  80. package/dist/core/skills/runtime/MicroTaskRunner.js +1 -1
  81. package/dist/core/skills/runtime/SkillRunner.js +5 -2
  82. package/dist/core/slash/steps/slash-execute.js +7 -5
  83. package/dist/core/slash/strategy.js +1 -1
  84. package/dist/core/strata/layers/worktree.js +7 -9
  85. package/dist/core/strata/runtime/synchronizer.js +10 -9
  86. package/dist/core/streaming/canonical/parts-from-llm-stream-chunk.js +1 -11
  87. package/dist/core/structured-output/json-schema-validator.js +1 -13
  88. package/dist/core/sub-agent/context-snapshot.js +12 -6
  89. package/dist/core/sub-agent/controller.js +70 -1
  90. package/dist/core/sub-agent/core/loop.js +25 -3
  91. package/dist/core/sub-agent/core/manager.js +319 -116
  92. package/dist/core/sub-agent/registry-defaults.js +12 -0
  93. package/dist/core/sub-agent/registry.js +8 -0
  94. package/dist/core/sub-agent/team.js +98 -0
  95. package/dist/core/sub-agent/tools/task-await.js +109 -0
  96. package/dist/core/sub-agent/tools/task-spawn.js +49 -7
  97. package/dist/core/sub-agent/tools/team.js +92 -0
  98. package/dist/core/sub-agent/types.js +11 -2
  99. package/dist/core/tools/budget.js +4 -11
  100. package/dist/core/tools/builtin/code-search/executor.js +46 -43
  101. package/dist/core/tools/builtin/fs.js +14 -6
  102. package/dist/core/tools/builtin/index.js +41 -107
  103. package/dist/core/tools/builtin/interaction.js +13 -15
  104. package/dist/core/tools/builtin/proposal.js +11 -2
  105. package/dist/core/tools/capability/executor.js +5 -5
  106. package/dist/core/tools/headless-payload.js +1 -3
  107. package/dist/core/tools/mapper.js +8 -42
  108. package/dist/core/tools/parallel/persistence.js +17 -5
  109. package/dist/core/tools/parallel/scheduler.js +23 -21
  110. package/dist/core/tools/permissions/permission-rules.js +66 -114
  111. package/dist/core/tools/plugins/loader.js +4 -3
  112. package/dist/core/tools/router.js +24 -53
  113. package/dist/core/tools/session.js +54 -97
  114. package/dist/core/tools/streaming/ToolCallAccumulator.js +1 -3
  115. package/dist/core/tools/tool-visibility.js +2 -1
  116. package/dist/core/tools/types.js +10 -0
  117. package/dist/core/utils/error.js +79 -0
  118. package/dist/core/utils/serialize.js +63 -0
  119. package/dist/core/utils/zod.js +29 -0
  120. package/dist/core/workspace/capabilities.js +3 -2
  121. package/dist/integrations/langfuse/litellm-langfuse-outcome-reporter.js +9 -8
  122. package/dist/locales/en.js +2 -1
  123. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { isRecord } from '../../utils/serialize.js';
2
3
  import { jsonSchemaToZod } from '../schema/json-schema-to-zod.js';
3
4
  const SAFE_TOKEN_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
4
5
  function normalizeToken(value) {
@@ -16,11 +17,11 @@ function safeToken(value, fallback) {
16
17
  return fallback;
17
18
  }
18
19
  function isPromptOptions(value) {
19
- return Boolean(value &&
20
- typeof value === 'object' &&
20
+ return Boolean(isRecord(value) &&
21
21
  'serverName' in value &&
22
22
  'client' in value &&
23
- typeof value.client?.listPrompts === 'function');
23
+ isRecord(value.client) &&
24
+ typeof value.client.listPrompts === 'function');
24
25
  }
25
26
  function buildFallbackSchemaFromArguments(args = []) {
26
27
  const shape = {};
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { LIMITS } from '../../config/limits.js';
3
3
  import { Phase } from '../../types/runtime.js';
4
+ import { safeStringify } from '../../utils/serialize.js';
4
5
  import { classifyMcpTool } from '../policy/classifier.js';
5
6
  import { jsonSchemaToZod } from '../schema/json-schema-to-zod.js';
6
7
  const MCP_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_.-]*$/;
@@ -111,10 +112,9 @@ export function wrapMcpToolResult(result) {
111
112
  }
112
113
  export function mcpToolToToolSpec(input) {
113
114
  const override = findOverride(input.server.capabilities, input.tool.name);
114
- const classification = classifyMcpTool({
115
- tool: input.tool,
115
+ const classification = classifyMcpTool(input.tool, {
116
116
  trust: input.server.trust,
117
- override,
117
+ override: override ? { sideEffects: override } : undefined,
118
118
  });
119
119
  const phase = input.server.capabilities.tools.phases[0] ?? Phase.VERIFY;
120
120
  const grantDecision = input.policy.decideTool({
@@ -169,7 +169,7 @@ export async function registerMcpV2Tools(input) {
169
169
  if (!catalog)
170
170
  continue;
171
171
  for (const tool of catalog.tools) {
172
- const classification = classifyMcpTool({ tool: tool, trust: server.trust });
172
+ const classification = classifyMcpTool(tool, { trust: server.trust });
173
173
  const decision = input.policy.decideTool({
174
174
  server: server.name,
175
175
  toolName: tool.name,
@@ -267,16 +267,7 @@ function summarizeMcpAuthorization(input, args, riskLevel, sideEffects) {
267
267
  : { kind: 'classified', reason: input.classification.reason },
268
268
  args,
269
269
  };
270
- return safeStringify(payload);
271
- }
272
- function safeStringify(value, maxLength = 1200) {
273
- try {
274
- const raw = JSON.stringify(value);
275
- return raw.length <= maxLength ? raw : `${raw.slice(0, maxLength)}...`;
276
- }
277
- catch {
278
- return '[Unserializable]';
279
- }
270
+ return safeStringify(payload, { maxLength: 1200 });
280
271
  }
281
272
  function coerceRecord(input) {
282
273
  if (!input || typeof input !== 'object' || Array.isArray(input)) {
@@ -2,6 +2,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
2
  import { CallToolResultSchema, PromptListChangedNotificationSchema, ReadResourceResultSchema, ResourceListChangedNotificationSchema, ResourceUpdatedNotificationSchema, ToolListChangedNotificationSchema, GetPromptResultSchema, } from '@modelcontextprotocol/sdk/types.js';
3
3
  import { LIMITS } from '../../config/limits.js';
4
4
  import { getLogger } from '../../observability/logger.js';
5
+ import { errorMessage } from '../../utils/error.js';
5
6
  import { PACKAGE_VERSION } from '../../version.js';
6
7
  import { discoverMcpCatalog } from '../catalog/discovery.js';
7
8
  import { McpNotificationRouter } from '../catalog/notification-router.js';
@@ -93,7 +94,7 @@ export class McpConnectionManager {
93
94
  }
94
95
  catch (error) {
95
96
  entry.status = 'degraded';
96
- entry.error = error instanceof Error ? error.message : String(error);
97
+ entry.error = errorMessage(error);
97
98
  getLogger().warn(`Failed to connect MCP server ${server.name}: ${entry.error}`);
98
99
  }
99
100
  return this.view(entry);
@@ -183,7 +184,7 @@ export class McpConnectionManager {
183
184
  entry.subscribedResources.add(uri);
184
185
  }
185
186
  catch (error) {
186
- const message = error instanceof Error ? error.message : String(error);
187
+ const message = errorMessage(error);
187
188
  getLogger().warn(`MCP server ${entry.server.name} resource subscription failed for ${uri}: ${message}`);
188
189
  }
189
190
  }
@@ -120,7 +120,7 @@ export class McpSamplingProvider {
120
120
  throw new Error('MCP_SAMPLING_TOKEN_LIMIT_EXCEEDED');
121
121
  }
122
122
  const result = await this.llm.chat([{ role: 'user', content: input.prompt }]);
123
- return typeof result === 'string' ? result : String(result?.content ?? '');
123
+ return typeof result === 'string' ? result : String(result.content ?? '');
124
124
  }
125
125
  async createMessage(params, options = {}) {
126
126
  const sanitizedParams = sanitizeForAudit(params, this.maxDepth);
@@ -1,5 +1,6 @@
1
1
  import { isDeepStrictEqual } from 'node:util';
2
2
  import { z } from 'zod';
3
+ import { isRecord } from '../../utils/serialize.js';
3
4
  export function jsonSchemaToZod(jsonSchema) {
4
5
  return jsonSchemaToZodWithContext(jsonSchema, jsonSchema);
5
6
  }
@@ -499,7 +500,7 @@ function propertyNamesToZod(jsonSchema, rootSchema) {
499
500
  return jsonSchemaToZodWithContext(jsonSchema, rootSchema);
500
501
  }
501
502
  function isPlainObject(value) {
502
- return typeof value === 'object' && value !== null && !Array.isArray(value);
503
+ return isRecord(value);
503
504
  }
504
505
  function isMultipleOf(value, divisor) {
505
506
  const quotient = value / divisor;
@@ -77,11 +77,12 @@ export function buildRelevantMemoryCandidates(context) {
77
77
  tags: ['rules', 'project'],
78
78
  });
79
79
  }
80
- if (trimToUndefined(knowledge?.user_preferences)) {
80
+ const userPrefs = trimToUndefined(knowledge?.user_preferences);
81
+ if (userPrefs) {
81
82
  candidates.push({
82
83
  path: '.salmonloop/knowledge/user_preferences',
83
84
  title: 'User preferences',
84
- summary: knowledge.user_preferences,
85
+ summary: userPrefs,
85
86
  tags: ['preferences', 'user'],
86
87
  });
87
88
  }
@@ -96,11 +97,12 @@ export function buildRelevantMemoryCandidates(context) {
96
97
  tags: ['architecture', ...(decision.related_files ?? []).map((file) => file.toLowerCase())],
97
98
  });
98
99
  }
99
- if (trimToUndefined(metadata?.aiInstructions)) {
100
+ const aiInstructions = trimToUndefined(metadata?.aiInstructions);
101
+ if (aiInstructions) {
100
102
  candidates.push({
101
103
  path: '.salmonloop/project/ai-instructions',
102
104
  title: 'Project AI instructions',
103
- summary: metadata.aiInstructions,
105
+ summary: aiInstructions,
104
106
  tags: ['instructions', 'project'],
105
107
  });
106
108
  }
@@ -1,3 +1,4 @@
1
+ import { asRecord } from '../utils/serialize.js';
1
2
  import { getAuditTrail } from './audit-trail.js';
2
3
  function safeString(value) {
3
4
  if (typeof value !== 'string')
@@ -23,13 +24,13 @@ export function extractAuthorizationDecisionsFromAuditTrail(auditTrail) {
23
24
  continue;
24
25
  if (event.action !== 'authorization.decision')
25
26
  continue;
26
- const details = event.details;
27
- if (!details || typeof details !== 'object')
27
+ if (!event.details || typeof event.details !== 'object')
28
28
  continue;
29
- const callId = safeString(details.callId);
30
- const toolName = safeString(details.toolName);
31
- const phase = safeString(details.phase) ?? safeString(event.phase);
32
- const outcome = safeString(details.outcome);
29
+ const d = asRecord(event.details);
30
+ const callId = safeString(d.callId);
31
+ const toolName = safeString(d.toolName);
32
+ const phase = safeString(d.phase) ?? safeString(event.phase);
33
+ const outcome = safeString(d.outcome);
33
34
  if (!callId || !toolName || !phase || !outcome)
34
35
  continue;
35
36
  decisions.push({
@@ -37,12 +38,12 @@ export function extractAuthorizationDecisionsFromAuditTrail(auditTrail) {
37
38
  toolName,
38
39
  phase: phase,
39
40
  outcome: outcome,
40
- source: (safeString(details.source) ?? 'unknown'),
41
- reason: safeString(details.reason),
42
- ttlMs: safeNumber(details.ttlMs),
43
- persist: safeString(details.persist),
44
- riskLevel: safeString(details.riskLevel),
45
- sideEffects: safeStringArray(details.sideEffects),
41
+ source: (safeString(d.source) ?? 'unknown'),
42
+ reason: safeString(d.reason),
43
+ ttlMs: safeNumber(d.ttlMs),
44
+ persist: safeString(d.persist),
45
+ riskLevel: safeString(d.riskLevel),
46
+ sideEffects: safeStringArray(d.sideEffects),
46
47
  timestamp: safeString(event.timestamp) ?? new Date().toISOString(),
47
48
  });
48
49
  }
@@ -1,4 +1,5 @@
1
1
  import { text } from '../../locales/index.js';
2
+ import { isRecord } from '../utils/serialize.js';
2
3
  import { getAuditTrail } from './audit-trail.js';
3
4
  import { REDACTED_ERROR_TOKEN } from './error-envelope.js';
4
5
  function mapLlmCodeToMessage(code) {
@@ -193,7 +194,7 @@ export function mapErrorForAudit(input) {
193
194
  };
194
195
  }
195
196
  function buildLangfuseHttpFailed(details) {
196
- const status = typeof details?.status === 'number' ? details.status : undefined;
197
+ const status = isRecord(details) && typeof details.status === 'number' ? details.status : undefined;
197
198
  if (!status)
198
199
  return undefined;
199
200
  if (status === 401 || status === 403) {
@@ -1,3 +1,4 @@
1
+ import { asRecord } from '../utils/serialize.js';
1
2
  import { getAuditTrail } from './audit-trail.js';
2
3
  function safeFiniteNumber(value) {
3
4
  if (typeof value !== 'number' || !Number.isFinite(value))
@@ -12,11 +13,11 @@ export function extractTokenUsageFromAuditTrail(auditTrail) {
12
13
  continue;
13
14
  if (event.action !== 'llm.usage')
14
15
  continue;
15
- const details = event.details;
16
- if (!details || typeof details !== 'object')
16
+ if (!event.details || typeof event.details !== 'object')
17
17
  continue;
18
- const promptTokens = safeFiniteNumber(details.promptTokens);
19
- const completionTokens = safeFiniteNumber(details.completionTokens);
18
+ const d = asRecord(event.details);
19
+ const promptTokens = safeFiniteNumber(d.promptTokens);
20
+ const completionTokens = safeFiniteNumber(d.completionTokens);
20
21
  if (typeof promptTokens === 'number')
21
22
  inputTokens += promptTokens;
22
23
  if (typeof completionTokens === 'number')
@@ -2,6 +2,7 @@ import { join } from 'path';
2
2
  import { typescriptPlugin, tsxPlugin, javascriptPlugin } from '../../languages/typescript/index.js';
3
3
  import { readdir } from '../adapters/fs/node-fs.js';
4
4
  import { getLogger } from '../observability/logger.js';
5
+ import { errorMessage } from '../utils/error.js';
5
6
  import { validateQueryPack } from './validator.js';
6
7
  // Import built-in plugins (Phase 1: explicit import)
7
8
  export class PluginLoader {
@@ -31,11 +32,11 @@ export class PluginLoader {
31
32
  catch (error) {
32
33
  // In test environment, we want to know why it failed
33
34
  if (process.env.NODE_ENV === 'test') {
34
- const errorMsg = error instanceof Error ? error.message : String(error);
35
+ const errorMsg = errorMessage(error);
35
36
  getLogger().error(`CRITICAL: Failed to load plugins: ${errorMsg}`);
36
37
  throw error;
37
38
  }
38
- getLogger().error(`Failed to load plugins: ${error instanceof Error ? error.message : String(error)}`);
39
+ getLogger().error(`Failed to load plugins: ${errorMessage(error)}`);
39
40
  }
40
41
  }
41
42
  /**
@@ -70,7 +71,7 @@ export class PluginLoader {
70
71
  }
71
72
  catch (err) {
72
73
  if (err && typeof err === 'object' && 'code' in err && err.code !== 'ENOENT') {
73
- getLogger().warn(`Failed to load user plugin from ${dirName}: ${err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err)}`);
74
+ getLogger().warn(`Failed to load user plugin from ${dirName}: ${errorMessage(err)}`);
74
75
  }
75
76
  }
76
77
  }
@@ -78,7 +79,7 @@ export class PluginLoader {
78
79
  catch (err) {
79
80
  // Ignore if directory doesn't exist
80
81
  if (err && typeof err === 'object' && 'code' in err && err.code !== 'ENOENT') {
81
- getLogger().debug(`Error scanning for user plugins: ${err instanceof Error ? err.message : String(err)}`);
82
+ getLogger().debug(`Error scanning for user plugins: ${errorMessage(err)}`);
82
83
  }
83
84
  }
84
85
  }
@@ -2,6 +2,7 @@ import { fileURLToPath } from 'node:url';
2
2
  import Handlebars from 'handlebars';
3
3
  import { z } from 'zod';
4
4
  import { readFile } from '../adapters/fs/node-fs.js';
5
+ import { unwrapZodSchema } from '../utils/zod.js';
5
6
  const TEMPLATE_URLS = {
6
7
  'system/_tool_defs.hbs': new URL('./templates/system/_tool_defs.hbs', import.meta.url),
7
8
  'system/main_system.hbs': new URL('./templates/system/main_system.hbs', import.meta.url),
@@ -60,8 +61,8 @@ export class PromptRegistry {
60
61
  if (!url) {
61
62
  throw new Error(`Unknown prompt template path: ${relativePath}`);
62
63
  }
63
- const bunAny = globalThis;
64
- const bun = bunAny.Bun;
64
+ const bunGlobal = globalThis;
65
+ const bun = bunGlobal.Bun;
65
66
  if (bun?.file) {
66
67
  return bun.file(url).text();
67
68
  }
@@ -113,34 +114,15 @@ export class PromptRegistry {
113
114
  if (!zodSchema) {
114
115
  return { type: 'object', description: 'Schema details unavailable' };
115
116
  }
116
- const unwrapForJsonSchema = (schema) => {
117
- let current = schema;
118
- for (let depth = 0; depth < 20; depth++) {
119
- const ZodEffects = z.ZodEffects;
120
- if (ZodEffects && current instanceof ZodEffects) {
121
- current = current._def.schema;
122
- continue;
123
- }
124
- if (current instanceof z.ZodPipe) {
125
- current = current._def.out;
126
- continue;
127
- }
128
- if (current instanceof z.ZodOptional ||
129
- current instanceof z.ZodNullable ||
130
- current instanceof z.ZodDefault) {
131
- current = current._def.innerType;
132
- continue;
133
- }
134
- break;
135
- }
136
- return current;
137
- };
138
117
  try {
139
- const unwrapped = unwrapForJsonSchema(zodSchema);
140
- const schema = z.toJSONSchema(unwrapped);
141
- if (schema && typeof schema === 'object') {
142
- const { $schema: _ignored, ...rest } = schema;
143
- return rest;
118
+ const unwrapped = unwrapZodSchema(zodSchema);
119
+ const zWithJson = z;
120
+ if (zWithJson.toJSONSchema) {
121
+ const schema = zWithJson.toJSONSchema(unwrapped);
122
+ if (schema && typeof schema === 'object') {
123
+ const { $schema: _ignored, ...rest } = schema;
124
+ return rest;
125
+ }
144
126
  }
145
127
  }
146
128
  catch (_e) {
@@ -406,9 +406,8 @@ const normalizeA2AExtensionHeadersByVersion = (req, res, next) => {
406
406
  });
407
407
  next();
408
408
  };
409
- function isObjectRecord(value) {
410
- return typeof value === 'object' && value !== null && !Array.isArray(value);
411
- }
409
+ import { isRecord } from '../../../utils/serialize.js';
410
+ const isObjectRecord = isRecord;
412
411
  function looksLikeTask(result) {
413
412
  return typeof result.id === 'string' && 'status' in result;
414
413
  }
@@ -7,6 +7,7 @@ import { toAcpPublicModes } from '../../public-capabilities/projections.js';
7
7
  import { buildPublicCapabilityRegistry } from '../../public-capabilities/registry.js';
8
8
  import { parseSlashInput } from '../../slash/parser.js';
9
9
  import { Phase } from '../../types/runtime.js';
10
+ import { isRecord } from '../../utils/serialize.js';
10
11
  import { buildCanonicalExecutionRequest } from '../shared/execution-request.js';
11
12
  import { parseAcpFlowMode } from '../shared/flow-mode-mapping.js';
12
13
  import { probeCheckpoint, probeCheckpointForNewSession, } from './acp-checkpoint-probe.js';
@@ -18,7 +19,7 @@ import { createAcpSessionStore, isTerminalTaskEvent } from './handlers.js';
18
19
  import { createAcpToolAuthorizationProvider } from './permission-provider.js';
19
20
  import { mapToolKind } from './tool-kind-mapping.js';
20
21
  function formatInputRequiredMessage(inputRequired) {
21
- if (!inputRequired || !Array.isArray(inputRequired.questions))
22
+ if (!inputRequired || !isRecord(inputRequired) || !Array.isArray(inputRequired.questions))
22
23
  return null;
23
24
  const questions = inputRequired.questions;
24
25
  if (questions.length === 0)
@@ -525,6 +526,7 @@ function acpMcpServersToExtensions(mcpServers) {
525
526
  mcpServers: resolvedServers,
526
527
  toolPlugins: [],
527
528
  skillDiscovery: { paths: [], scope: 'repo' },
529
+ agentProfiles: [],
528
530
  };
529
531
  }
530
532
  function validateAcpMcpServers(mcpServers) {
@@ -569,12 +571,13 @@ function isPersistableSession(session) {
569
571
  async function awaitTerminalEvent(params) {
570
572
  if (!params.eventBus)
571
573
  return null;
572
- const history = params.eventBus.list(params.taskId);
574
+ const { eventBus } = params;
575
+ const history = eventBus.list(params.taskId);
573
576
  const terminal = history.find(isTerminalTaskEvent);
574
577
  if (terminal)
575
578
  return terminal;
576
579
  return await new Promise((resolve) => {
577
- const unsubscribe = params.eventBus.subscribe((event) => {
580
+ const unsubscribe = eventBus.subscribe((event) => {
578
581
  if (event.taskId !== params.taskId)
579
582
  return;
580
583
  if (!isTerminalTaskEvent(event))
@@ -1092,7 +1095,10 @@ export function createAcpFormalAgent(deps) {
1092
1095
  ...current,
1093
1096
  history: [
1094
1097
  ...current.history,
1095
- { role: 'assistant', content: [buildTextContentBlock(responseText)] },
1098
+ {
1099
+ role: 'assistant',
1100
+ content: [buildTextContentBlock(responseText)],
1101
+ },
1096
1102
  ],
1097
1103
  }));
1098
1104
  await sessionPersistence.persist();
@@ -1,6 +1,8 @@
1
1
  import { Readable, Writable } from 'node:stream';
2
2
  import { AgentSideConnection } from '@agentclientprotocol/sdk';
3
3
  import { tryGetLogger } from '../../observability/logger.js';
4
+ import { errorMessage } from '../../utils/error.js';
5
+ import { isRecord } from '../../utils/serialize.js';
4
6
  const INVALID_REQUEST = {
5
7
  jsonrpc: '2.0',
6
8
  id: null,
@@ -11,9 +13,7 @@ const PARSE_ERROR = {
11
13
  id: null,
12
14
  error: { code: -32700, message: 'Parse error' },
13
15
  };
14
- function isJsonObject(value) {
15
- return typeof value === 'object' && value !== null && !Array.isArray(value);
16
- }
16
+ const isJsonObject = isRecord;
17
17
  function hasOwn(value, key) {
18
18
  return Object.prototype.hasOwnProperty.call(value, key);
19
19
  }
@@ -50,9 +50,9 @@ function createNdjsonWriter(output) {
50
50
  .catch(() => undefined)
51
51
  .then(() => writer.write(data))
52
52
  .catch((error) => {
53
- const detail = error instanceof Error ? error.message : String(error);
53
+ const detail = errorMessage(error);
54
54
  safeWarn(`ACP stdio failed to write NDJSON line. reason="${detail}"`);
55
- lastError = error instanceof Error ? new Error(detail) : new Error(detail);
55
+ lastError = new Error(detail);
56
56
  });
57
57
  await tail;
58
58
  },
@@ -83,7 +83,7 @@ async function processStdioLine(line, ndjson, controller) {
83
83
  controller.enqueue(parsed);
84
84
  }
85
85
  catch (error) {
86
- const detail = error instanceof Error ? error.message : String(error);
86
+ const detail = errorMessage(error);
87
87
  safeWarn(`ACP stdio failed to parse JSON line. reason="${detail}"`);
88
88
  await ndjson.write(PARSE_ERROR);
89
89
  }
@@ -65,12 +65,13 @@ export function createAgentServerRuntime(deps) {
65
65
  }
66
66
  }
67
67
  async function close() {
68
- if (!a2aServerInstance) {
68
+ const instance = a2aServerInstance;
69
+ if (!instance) {
69
70
  started = false;
70
71
  return;
71
72
  }
72
73
  await new Promise((resolve, reject) => {
73
- a2aServerInstance.close((error) => {
74
+ instance.close((error) => {
74
75
  if (error) {
75
76
  reject(error);
76
77
  return;
@@ -1,19 +1,24 @@
1
+ import { readFileSync } from '../adapters/fs/node-fs.js';
1
2
  import { initializeDefaultCalculator } from '../context/policies/pack-until-full.js';
3
+ import { getRepoAgentsConfigPath, getUserAgentsConfigPath } from '../extensions/paths.js';
4
+ import { AgentsConfigSchema } from '../extensions/schemas.js';
2
5
  import { createLogger, getLogger, setLogger, tryGetLogger } from '../observability/logger.js';
3
6
  import { createMonitor, setMonitor, tryGetMonitor } from '../observability/monitor.js';
4
7
  import { registerDefaultSubAgentProfiles } from '../sub-agent/registry-defaults.js';
5
8
  import { createSubAgentRegistry, setSubAgentRegistry, tryGetSubAgentRegistry, } from '../sub-agent/registry.js';
9
+ import { isRecord } from '../utils/serialize.js';
6
10
  /**
7
11
  * Initializes the Core safety runtime.
8
12
  * Mounts global error handlers and ensures environment safety.
9
13
  */
14
+ const GLOBAL_FLAG = '__SALMON_RUNTIME_INITIALIZED__';
10
15
  export function initializeRuntime() {
11
16
  // Prevent duplicate initialization
12
- if (globalThis.__SALMON_RUNTIME_INITIALIZED__)
17
+ if (globalThis[GLOBAL_FLAG])
13
18
  return;
14
19
  // Bypass interception in debug mode to allow raw console/stream output
15
20
  if (process.env.SALMONLOOP_DEBUG === 'true') {
16
- globalThis.__SALMON_RUNTIME_INITIALIZED__ = true;
21
+ globalThis[GLOBAL_FLAG] = true;
17
22
  return;
18
23
  }
19
24
  // Preload token calculator in background (non-blocking)
@@ -32,6 +37,7 @@ export function initializeRuntime() {
32
37
  if (!tryGetSubAgentRegistry()) {
33
38
  const registry = createSubAgentRegistry();
34
39
  registerDefaultSubAgentProfiles(registry);
40
+ loadUserAgentProfiles(registry);
35
41
  setSubAgentRegistry(registry);
36
42
  }
37
43
  const isGui = process.argv.includes('--gui');
@@ -40,10 +46,12 @@ export function initializeRuntime() {
40
46
  const originalConsoleError = console.error;
41
47
  console.error = (...args) => {
42
48
  const sanitizedArgs = args.map((arg) => {
43
- if (typeof arg === 'object' && arg !== null) {
49
+ if (isRecord(arg)) {
44
50
  // Drop the object structure entirely for console output to prevent UI pollution
45
- const code = arg.code || arg.llmCode || 'TECHNICAL_ERROR';
46
- const msg = arg.message || 'No detail provided';
51
+ const code = (typeof arg.code === 'string' ? arg.code : undefined) ||
52
+ (typeof arg.llmCode === 'string' ? arg.llmCode : undefined) ||
53
+ 'TECHNICAL_ERROR';
54
+ const msg = (typeof arg.message === 'string' ? arg.message : undefined) || 'No detail provided';
47
55
  return `[${code}] ${msg}`;
48
56
  }
49
57
  return arg;
@@ -127,6 +135,62 @@ export function initializeRuntime() {
127
135
  process.on('uncaughtException', (error) => {
128
136
  getLogger().error('Uncaught Exception detected in Core runtime', error, true);
129
137
  });
130
- globalThis.__SALMON_RUNTIME_INITIALIZED__ = true;
138
+ globalThis[GLOBAL_FLAG] = true;
139
+ }
140
+ function loadUserAgentProfiles(registry) {
141
+ const tryLoadSync = (filePath) => {
142
+ try {
143
+ const content = readFileSync(filePath, 'utf-8');
144
+ return AgentsConfigSchema.parse(JSON.parse(content));
145
+ }
146
+ catch {
147
+ return null;
148
+ }
149
+ };
150
+ // Load from repo and user scopes; repo takes priority
151
+ const repoRoot = process.cwd();
152
+ const userConfig = tryLoadSync(getUserAgentsConfigPath());
153
+ const repoConfig = tryLoadSync(getRepoAgentsConfigPath(repoRoot));
154
+ const toProfile = (raw) => ({
155
+ id: raw.id,
156
+ name: raw.name,
157
+ role: raw.role,
158
+ description: raw.description,
159
+ allowedTools: raw.allowedTools ?? ['code.search', 'fs.read'],
160
+ readOnly: raw.readOnly ?? false,
161
+ stratagem: raw.stratagem ?? 'investigator',
162
+ toolInheritance: raw.toolInheritance,
163
+ permissionMode: raw.permissionMode,
164
+ systemPrompt: raw.systemPrompt,
165
+ maxTokens: raw.maxTokens,
166
+ maxAttempts: raw.maxAttempts,
167
+ timeoutMs: raw.timeoutMs,
168
+ });
169
+ // User profiles first (lower priority)
170
+ if (userConfig) {
171
+ for (const agent of userConfig.agents) {
172
+ if (agent.enabled === false)
173
+ continue;
174
+ // Don't override built-in profiles
175
+ if (registry.has(agent.id)) {
176
+ tryGetLogger()?.debug(`[initializeRuntime] Skipping user agent '${agent.id}': conflicts with built-in profile`);
177
+ continue;
178
+ }
179
+ registry.register(toProfile(agent));
180
+ }
181
+ }
182
+ // Repo profiles override user (higher priority)
183
+ if (repoConfig) {
184
+ for (const agent of repoConfig.agents) {
185
+ if (agent.enabled === false)
186
+ continue;
187
+ // Don't override built-in profiles
188
+ if (registry.has(agent.id)) {
189
+ tryGetLogger()?.debug(`[initializeRuntime] Skipping repo agent '${agent.id}': conflicts with built-in profile`);
190
+ continue;
191
+ }
192
+ registry.register(toProfile(agent));
193
+ }
194
+ }
131
195
  }
132
196
  //# sourceMappingURL=initialize.js.map
@@ -1,6 +1,7 @@
1
1
  import { getModelRecommendedBudget } from '../../context/token/adaptive-budget.js';
2
2
  import { LlmError } from '../../llm/errors.js';
3
3
  import { getLogger } from '../../observability/logger.js';
4
+ import { isRecord } from '../../utils/serialize.js';
4
5
  import { refreshSessionSummary } from '../summary-sync.js';
5
6
  import { TokenTracker } from '../token-tracker.js';
6
7
  import { isCircuitBreakerTripped, onCompactionFailure, onCompactionSuccess } from './tracking.js';
@@ -11,8 +12,8 @@ function isContextOverflowLike(error) {
11
12
  }
12
13
  const message = error instanceof Error
13
14
  ? error.message
14
- : error && typeof error === 'object' && typeof error.message === 'string'
15
- ? String(error.message)
15
+ : isRecord(error) && typeof error.message === 'string'
16
+ ? error.message
16
17
  : '';
17
18
  if (!message)
18
19
  return false;
@@ -136,7 +137,7 @@ export async function autocompact(params) {
136
137
  performed: true,
137
138
  tracking: onCompactionSuccess(tracking),
138
139
  preTokens: totalTokens,
139
- trigger: trigger,
140
+ trigger,
140
141
  };
141
142
  }
142
143
  catch (error) {