salmon-loop 0.2.3 → 0.2.13
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/chat.js +1 -0
- package/dist/cli/commands/chat.js +17 -18
- package/dist/cli/commands/context.js +15 -3
- package/dist/cli/commands/help-format.js +12 -0
- package/dist/cli/commands/registry.js +4 -7
- package/dist/cli/commands/run/config-resolution.js +30 -24
- package/dist/cli/commands/run/handler.js +16 -17
- package/dist/cli/commands/run/loop-params.js +1 -0
- package/dist/cli/commands/run/parse-options.js +2 -2
- package/dist/cli/commands/run/validate-options.js +0 -5
- package/dist/cli/commands/run/verbose.js +2 -7
- package/dist/cli/commands/serve.js +29 -22
- package/dist/cli/locales/en.js +2 -0
- package/dist/cli/program-bootstrap.js +6 -1
- package/dist/cli/program-commands.js +4 -0
- package/dist/cli/program-options.js +1 -0
- package/dist/cli/slash/runtime.js +3 -3
- package/dist/cli/utils/output-format.js +6 -0
- package/dist/cli/utils/resolve-cli-config.js +98 -0
- package/dist/cli/utils/verbose-level.js +8 -0
- package/dist/core/config/load.js +22 -8
- package/dist/core/config/merge.js +27 -0
- package/dist/core/config/paths.js +24 -5
- package/dist/core/config/resolve.js +7 -5
- package/dist/core/config/validate.js +21 -0
- package/dist/core/facades/cli-command-chat.js +1 -1
- package/dist/core/facades/cli-context.js +1 -0
- package/dist/core/grizzco/engine/transaction/transaction-runner.js +8 -0
- package/dist/core/grizzco/steps/preflight.js +4 -1
- package/dist/core/intent/chat-intent.js +0 -4
- package/dist/core/llm/ai-sdk/request-params.js +1 -1
- package/dist/core/protocols/a2a/sdk/executor.js +6 -5
- package/dist/core/protocols/acp/formal-agent.js +163 -20
- package/dist/core/protocols/acp/permission-provider.js +20 -0
- package/dist/core/protocols/shared/execution-request.js +24 -0
- package/dist/core/session/compression.js +4 -4
- package/dist/core/session/manager.js +3 -2
- package/dist/core/strata/layers/worktree.js +4 -4
- package/dist/core/tools/builtin/fs.js +4 -4
- package/dist/interfaces/cli/task-runner.js +4 -3
- package/dist/locales/en.js +52 -0
- package/package.json +3 -3
package/dist/core/config/load.js
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { readFile } from '../adapters/fs/node-fs.js';
|
|
2
2
|
import { ConfigError } from './errors.js';
|
|
3
3
|
import { parseConfigText } from './file-format.js';
|
|
4
|
-
import { getDefaultRepoConfigPaths, resolveConfigPath } from './paths.js';
|
|
4
|
+
import { getDefaultRepoConfigPaths, getDefaultUserConfigPaths, resolveConfigPath, } from './paths.js';
|
|
5
5
|
import { validateConfigFileV1 } from './validate.js';
|
|
6
|
-
|
|
7
|
-
if (!opts.enabled)
|
|
8
|
-
return null;
|
|
9
|
-
const candidatePaths = opts.configPath
|
|
10
|
-
? [resolveConfigPath(opts.repoRoot, opts.configPath)]
|
|
11
|
-
: getDefaultRepoConfigPaths(opts.repoRoot);
|
|
6
|
+
async function loadFromCandidates(candidatePaths, required) {
|
|
12
7
|
for (let i = 0; i < candidatePaths.length; i++) {
|
|
13
8
|
const absPath = candidatePaths[i];
|
|
14
9
|
try {
|
|
@@ -21,7 +16,7 @@ export async function tryLoadConfigFile(opts) {
|
|
|
21
16
|
if ((e && typeof e === 'object' && 'code' in e ? e.code : undefined) ===
|
|
22
17
|
'ENOENT') {
|
|
23
18
|
const isLast = i === candidatePaths.length - 1;
|
|
24
|
-
if (
|
|
19
|
+
if (required && isLast) {
|
|
25
20
|
throw new ConfigError('CONFIG_FILE_NOT_FOUND', { path: absPath });
|
|
26
21
|
}
|
|
27
22
|
continue;
|
|
@@ -31,4 +26,23 @@ export async function tryLoadConfigFile(opts) {
|
|
|
31
26
|
}
|
|
32
27
|
return null;
|
|
33
28
|
}
|
|
29
|
+
export async function tryLoadConfigFile(opts) {
|
|
30
|
+
if (!opts.enabled)
|
|
31
|
+
return null;
|
|
32
|
+
const candidatePaths = opts.configPath
|
|
33
|
+
? [resolveConfigPath(opts.repoRoot, opts.configPath)]
|
|
34
|
+
: getDefaultRepoConfigPaths(opts.repoRoot);
|
|
35
|
+
return loadFromCandidates(candidatePaths, opts.required);
|
|
36
|
+
}
|
|
37
|
+
export async function loadConfigStack(opts) {
|
|
38
|
+
if (!opts.enabled)
|
|
39
|
+
return {};
|
|
40
|
+
if (opts.configPath) {
|
|
41
|
+
const loaded = await tryLoadConfigFile(opts);
|
|
42
|
+
return loaded ? { repo: loaded } : {};
|
|
43
|
+
}
|
|
44
|
+
const repo = await loadFromCandidates(getDefaultRepoConfigPaths(opts.repoRoot), false);
|
|
45
|
+
const user = await loadFromCandidates(getDefaultUserConfigPaths(), false);
|
|
46
|
+
return { repo: repo ?? undefined, user: user ?? undefined };
|
|
47
|
+
}
|
|
34
48
|
//# sourceMappingURL=load.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function isPlainObject(value) {
|
|
2
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function mergeValues(userValue, repoValue) {
|
|
5
|
+
if (repoValue === undefined)
|
|
6
|
+
return userValue;
|
|
7
|
+
if (userValue === undefined)
|
|
8
|
+
return repoValue;
|
|
9
|
+
if (isPlainObject(userValue) && isPlainObject(repoValue)) {
|
|
10
|
+
const merged = { ...userValue };
|
|
11
|
+
for (const [key, value] of Object.entries(repoValue)) {
|
|
12
|
+
merged[key] = mergeValues(userValue[key], value);
|
|
13
|
+
}
|
|
14
|
+
return merged;
|
|
15
|
+
}
|
|
16
|
+
return repoValue;
|
|
17
|
+
}
|
|
18
|
+
export function mergeConfigFiles(userConfig, repoConfig) {
|
|
19
|
+
if (!userConfig && !repoConfig)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (!userConfig)
|
|
22
|
+
return repoConfig;
|
|
23
|
+
if (!repoConfig)
|
|
24
|
+
return userConfig;
|
|
25
|
+
return mergeValues(userConfig, repoConfig);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=merge.js.map
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { defaultPathAdapter } from '../adapters/path/path-adapter.js';
|
|
3
|
+
function resolveUserConfigHome() {
|
|
4
|
+
const override = (process.env.SALMONLOOP_USER_CONFIG_HOME || '').trim();
|
|
5
|
+
if (override)
|
|
6
|
+
return defaultPathAdapter.resolve(override);
|
|
7
|
+
return homedir();
|
|
8
|
+
}
|
|
2
9
|
/**
|
|
3
10
|
* Repo-local configuration lives under ".salmonloop/" and is expected to be gitignored.
|
|
4
11
|
* Runtime state is stored under ".salmonloop/runtime/" (audit, rejections, tmp, locks).
|
|
@@ -7,14 +14,26 @@ export function getDefaultRepoConfigPath(repoRoot) {
|
|
|
7
14
|
return getDefaultRepoConfigPaths(repoRoot)[0];
|
|
8
15
|
}
|
|
9
16
|
export function getDefaultRepoConfigPaths(repoRoot) {
|
|
10
|
-
const base = join(resolve(repoRoot), '.salmonloop', 'config');
|
|
11
|
-
return [
|
|
17
|
+
const base = defaultPathAdapter.join(defaultPathAdapter.resolve(repoRoot), '.salmonloop', 'config');
|
|
18
|
+
return [
|
|
19
|
+
defaultPathAdapter.join(base, 'config.yaml'),
|
|
20
|
+
defaultPathAdapter.join(base, 'config.yml'),
|
|
21
|
+
defaultPathAdapter.join(base, 'config.json'),
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
export function getDefaultUserConfigPaths() {
|
|
25
|
+
const base = defaultPathAdapter.join(resolveUserConfigHome(), '.salmonloop', 'config');
|
|
26
|
+
return [
|
|
27
|
+
defaultPathAdapter.join(base, 'config.yaml'),
|
|
28
|
+
defaultPathAdapter.join(base, 'config.yml'),
|
|
29
|
+
defaultPathAdapter.join(base, 'config.json'),
|
|
30
|
+
];
|
|
12
31
|
}
|
|
13
32
|
export function resolveConfigPath(repoRoot, configPath) {
|
|
14
33
|
// Relative paths are resolved against the target repo root (not the CLI's cwd).
|
|
15
|
-
return resolve(repoRoot, configPath);
|
|
34
|
+
return defaultPathAdapter.resolve(repoRoot, configPath);
|
|
16
35
|
}
|
|
17
36
|
export function getDefaultIndexPath(repoRoot) {
|
|
18
|
-
return join(resolve(repoRoot), '.salmonloop', 'index');
|
|
37
|
+
return defaultPathAdapter.join(defaultPathAdapter.resolve(repoRoot), '.salmonloop', 'index');
|
|
19
38
|
}
|
|
20
39
|
//# sourceMappingURL=paths.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveLlmOutputPolicy } from '../llm/output-policy.js';
|
|
2
|
-
import {
|
|
2
|
+
import { loadConfigStack } from './load.js';
|
|
3
|
+
import { mergeConfigFiles } from './merge.js';
|
|
3
4
|
import { getDefaultRepoConfigPath } from './paths.js';
|
|
4
5
|
import { resolveLlmFromConfig } from './resolve-llm.js';
|
|
5
6
|
import { resolveAstValidationStrictness } from './resolvers/ast-validation.js';
|
|
@@ -15,20 +16,21 @@ export async function resolveConfig(opts) {
|
|
|
15
16
|
const enabled = opts.enableConfigFile !== false;
|
|
16
17
|
const path = opts.configFilePath;
|
|
17
18
|
const required = Boolean(opts.configFilePath);
|
|
18
|
-
const loaded = await
|
|
19
|
+
const loaded = await loadConfigStack({
|
|
19
20
|
repoRoot: opts.repoRoot,
|
|
20
21
|
configPath: path,
|
|
21
22
|
enabled,
|
|
22
23
|
required,
|
|
23
24
|
});
|
|
24
|
-
const raw = loaded?.config;
|
|
25
|
+
const raw = mergeConfigFiles(loaded.user?.config, loaded.repo?.config);
|
|
25
26
|
const uiLogMode = resolveUiLogMode(raw);
|
|
26
27
|
const permissionMode = resolvePermissionMode(raw);
|
|
28
|
+
const sourcePath = loaded.repo?.path || loaded.user?.path || path || getDefaultRepoConfigPath(opts.repoRoot);
|
|
27
29
|
return {
|
|
28
30
|
source: {
|
|
29
31
|
enabled,
|
|
30
|
-
path:
|
|
31
|
-
used: Boolean(loaded),
|
|
32
|
+
path: sourcePath,
|
|
33
|
+
used: Boolean(loaded.repo || loaded.user),
|
|
32
34
|
},
|
|
33
35
|
raw,
|
|
34
36
|
permissionMode,
|
|
@@ -471,6 +471,27 @@ export function validateConfigFileV1(input) {
|
|
|
471
471
|
}
|
|
472
472
|
if (input.llm.activeModel !== undefined)
|
|
473
473
|
cfg.llm.activeModel = input.llm.activeModel;
|
|
474
|
+
const llmAny = input.llm;
|
|
475
|
+
if (llmAny.simpleModel !== undefined && !isString(llmAny.simpleModel)) {
|
|
476
|
+
throw new ConfigError('CONFIG_INVALID_LLM_SIMPLE_MODEL', { expected: 'string' });
|
|
477
|
+
}
|
|
478
|
+
if (llmAny.simpleModel !== undefined)
|
|
479
|
+
cfg.llm.simpleModel = llmAny.simpleModel;
|
|
480
|
+
if (llmAny.mediumModel !== undefined && !isString(llmAny.mediumModel)) {
|
|
481
|
+
throw new ConfigError('CONFIG_INVALID_LLM_MEDIUM_MODEL', { expected: 'string' });
|
|
482
|
+
}
|
|
483
|
+
if (llmAny.mediumModel !== undefined)
|
|
484
|
+
cfg.llm.mediumModel = llmAny.mediumModel;
|
|
485
|
+
if (llmAny.complexModel !== undefined && !isString(llmAny.complexModel)) {
|
|
486
|
+
throw new ConfigError('CONFIG_INVALID_LLM_COMPLEX_MODEL', { expected: 'string' });
|
|
487
|
+
}
|
|
488
|
+
if (llmAny.complexModel !== undefined)
|
|
489
|
+
cfg.llm.complexModel = llmAny.complexModel;
|
|
490
|
+
if (llmAny.reasoningModel !== undefined && !isString(llmAny.reasoningModel)) {
|
|
491
|
+
throw new ConfigError('CONFIG_INVALID_LLM_REASONING_MODEL', { expected: 'string' });
|
|
492
|
+
}
|
|
493
|
+
if (llmAny.reasoningModel !== undefined)
|
|
494
|
+
cfg.llm.reasoningModel = llmAny.reasoningModel;
|
|
474
495
|
if (input.llm.providers !== undefined) {
|
|
475
496
|
if (!isRecord(input.llm.providers)) {
|
|
476
497
|
throw new ConfigError('CONFIG_INVALID_LLM_PROVIDERS', { expected: 'object' });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { normalizePermissionMode, resolveConfig } from '../config/index.js';
|
|
1
|
+
export { ConfigError, normalizePermissionMode, resolveConfig } from '../config/index.js';
|
|
2
2
|
export { ExtensionConfigError, resolveExtensions } from '../extensions/index.js';
|
|
3
3
|
export { createRuntimeLlm } from '../llm/factory.js';
|
|
4
4
|
export { getLogger } from '../observability/logger.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { defaultPathAdapter } from '../adapters/path/path-adapter.js';
|
|
2
|
+
export { ConfigError } from '../config/index.js';
|
|
2
3
|
export { resolveConfig } from '../config/resolve.js';
|
|
3
4
|
export { createContextCacheStore } from '../context/cache/store-factory.js';
|
|
4
5
|
export { ContextService } from '../context/index.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { recordAuditEvent } from '../../../observability/audit-trail.js';
|
|
2
|
+
import { mapErrorForAudit } from '../../../observability/error-mapping.js';
|
|
2
3
|
import { ReflectionEngine } from '../../../reflection/engine.js';
|
|
3
4
|
import { executeSalmonLoopFlow } from '../../flows/SalmonLoopFlow.js';
|
|
4
5
|
import { resolveAttemptFailure } from './attempt-failure.js';
|
|
@@ -120,6 +121,10 @@ export class FlowTransactionRunner {
|
|
|
120
121
|
timestamp: this.params.now(),
|
|
121
122
|
});
|
|
122
123
|
}
|
|
124
|
+
const mappedAuditError = mapErrorForAudit({
|
|
125
|
+
message: attemptFailure.safeHint ?? attemptFailure.reason,
|
|
126
|
+
code: attemptFailure.errorCode ?? attemptFailure.reasonCode,
|
|
127
|
+
});
|
|
123
128
|
recordAuditEvent('loop.attempt.failure', {
|
|
124
129
|
attempt,
|
|
125
130
|
flowMode: this.params.flowMode,
|
|
@@ -131,6 +136,9 @@ export class FlowTransactionRunner {
|
|
|
131
136
|
failurePhase: attemptFailure.failurePhase,
|
|
132
137
|
retryable: attemptFailure.retryable,
|
|
133
138
|
errorCode: attemptFailure.errorCode,
|
|
139
|
+
errorSummary: mappedAuditError.summary,
|
|
140
|
+
errorCategory: mappedAuditError.category,
|
|
141
|
+
errorRedacted: mappedAuditError.redacted,
|
|
134
142
|
lastStep: result.lastStep,
|
|
135
143
|
}, {
|
|
136
144
|
phase: attemptFailure.failurePhase,
|
|
@@ -6,7 +6,10 @@ import { preflight } from '../../verification/runner.js';
|
|
|
6
6
|
import { resolveLlmToolCallingPolicy } from '../dsl/llm-strategy.js';
|
|
7
7
|
export const runPreflight = async (ctx) => {
|
|
8
8
|
const result = await preflight(ctx.workspace, ctx.emit, {
|
|
9
|
-
ignoreDirty: ctx.mode === 'review' ||
|
|
9
|
+
ignoreDirty: ctx.mode === 'review' ||
|
|
10
|
+
ctx.mode === 'research' ||
|
|
11
|
+
ctx.mode === 'answer' ||
|
|
12
|
+
ctx.options.permissionMode === 'yolo',
|
|
10
13
|
});
|
|
11
14
|
if (!result.ok) {
|
|
12
15
|
const reason = result.reason || text.loop.preflightFailedNotGit;
|
|
@@ -4,7 +4,7 @@ export function buildAiSdkRequestParams(params) {
|
|
|
4
4
|
messages: params.messages,
|
|
5
5
|
tools: params.tools,
|
|
6
6
|
temperature: params.options.temperature,
|
|
7
|
-
maxOutputTokens: params.options.maxTokens,
|
|
7
|
+
maxOutputTokens: params.options.maxTokens != null ? Number(params.options.maxTokens) : undefined,
|
|
8
8
|
stopSequences: params.options.stop,
|
|
9
9
|
toolChoice: (params.options.toolChoice === 'none'
|
|
10
10
|
? 'none'
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { InMemoryTaskStore } from '@a2a-js/sdk/server';
|
|
2
|
+
import { buildCanonicalExecutionRequest, buildInstructionFromParts, } from '../../shared/execution-request.js';
|
|
2
3
|
export function createA2AInteractionExecutor(deps) {
|
|
3
4
|
const store = deps.taskStore ?? new InMemoryTaskStore();
|
|
4
5
|
const metadataByTaskId = new Map();
|
|
@@ -41,12 +42,13 @@ export function createA2AInteractionExecutor(deps) {
|
|
|
41
42
|
}
|
|
42
43
|
};
|
|
43
44
|
try {
|
|
44
|
-
const
|
|
45
|
+
const executionRequest = buildCanonicalExecutionRequest({
|
|
45
46
|
capability,
|
|
46
|
-
|
|
47
|
+
instruction: extractInstruction(requestContext.userMessage),
|
|
47
48
|
// Pass SDK's taskId to facade to ensure consistency with eventBusManager
|
|
48
49
|
taskId: requestContext.taskId,
|
|
49
50
|
});
|
|
51
|
+
const { task } = await deps.facade.createTask(executionRequest);
|
|
50
52
|
resolvedTaskId = task.id;
|
|
51
53
|
cleanupByTaskId.set(task.id, cleanup);
|
|
52
54
|
metadataByTaskId.set(task.id, {
|
|
@@ -216,9 +218,8 @@ export function createA2AInteractionExecutor(deps) {
|
|
|
216
218
|
function extractInstruction(message) {
|
|
217
219
|
const textParts = message.parts
|
|
218
220
|
.filter((part) => part.kind === 'text')
|
|
219
|
-
.map((part) => part.text
|
|
220
|
-
|
|
221
|
-
return textParts.join('\n') || 'Run task';
|
|
221
|
+
.map((part) => part.text);
|
|
222
|
+
return buildInstructionFromParts(textParts, { fallbackInstruction: 'Run task' });
|
|
222
223
|
}
|
|
223
224
|
function delay(ms) {
|
|
224
225
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -7,6 +7,7 @@ import { inferTurnStopReasonFromFailure } from '../../interaction/turn-stop-reas
|
|
|
7
7
|
import { recordAuditEvent } from '../../observability/audit-trail.js';
|
|
8
8
|
import { readPlan } from '../../plan/index.js';
|
|
9
9
|
import { parseSlashInput } from '../../slash/parser.js';
|
|
10
|
+
import { buildCanonicalExecutionRequest } from '../shared/execution-request.js';
|
|
10
11
|
import { createAcpCommandRunner } from './acp-command-runner.js';
|
|
11
12
|
import { createAcpFileSystem } from './acp-filesystem.js';
|
|
12
13
|
import { createAcpSessionStore, isTerminalTaskEvent } from './handlers.js';
|
|
@@ -52,6 +53,16 @@ function isAbsolutePath(filePath) {
|
|
|
52
53
|
return true; // UNC path
|
|
53
54
|
return false;
|
|
54
55
|
}
|
|
56
|
+
function deriveSessionTitleFromCwd(cwd) {
|
|
57
|
+
const trimmed = cwd.replace(/[\\/]+$/, '');
|
|
58
|
+
if (!trimmed)
|
|
59
|
+
return cwd;
|
|
60
|
+
const segments = trimmed.split(/[\\/]/).filter(Boolean);
|
|
61
|
+
const basename = segments.at(-1);
|
|
62
|
+
if (basename && basename.trim())
|
|
63
|
+
return basename;
|
|
64
|
+
return trimmed;
|
|
65
|
+
}
|
|
55
66
|
function ensureMarkdownParagraphBreak(text) {
|
|
56
67
|
if (!text)
|
|
57
68
|
return text;
|
|
@@ -71,11 +82,6 @@ function buildJsonResourceContentBlock(data) {
|
|
|
71
82
|
},
|
|
72
83
|
};
|
|
73
84
|
}
|
|
74
|
-
const defaultPromptCapabilities = {
|
|
75
|
-
image: false,
|
|
76
|
-
audio: false,
|
|
77
|
-
embeddedContext: false,
|
|
78
|
-
};
|
|
79
85
|
const ACP_AVAILABLE_COMMANDS = [
|
|
80
86
|
{ name: 'help', description: text.acp.slashHelpDescription },
|
|
81
87
|
];
|
|
@@ -294,12 +300,42 @@ function buildAvailableCommandsUpdateIfChanged(state) {
|
|
|
294
300
|
availableCommands,
|
|
295
301
|
};
|
|
296
302
|
}
|
|
303
|
+
function buildSessionInfoUpdateIfChanged(session, state) {
|
|
304
|
+
const title = typeof session.title === 'string' ? session.title : null;
|
|
305
|
+
const updatedAt = typeof session.updatedAt === 'string' ? session.updatedAt : null;
|
|
306
|
+
const digest = JSON.stringify({ title, updatedAt });
|
|
307
|
+
if (digest === state.lastSessionInfoDigest)
|
|
308
|
+
return null;
|
|
309
|
+
state.lastSessionInfoDigest = digest;
|
|
310
|
+
return {
|
|
311
|
+
sessionUpdate: 'session_info_update',
|
|
312
|
+
title,
|
|
313
|
+
updatedAt,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
297
316
|
function isSessionModeId(value) {
|
|
298
317
|
return value === 'interactive' || value === 'yolo';
|
|
299
318
|
}
|
|
300
319
|
function buildCurrentModeUpdate(modeId) {
|
|
301
320
|
return { sessionUpdate: 'current_mode_update', currentModeId: modeId };
|
|
302
321
|
}
|
|
322
|
+
function buildModesState(modeId) {
|
|
323
|
+
return {
|
|
324
|
+
currentModeId: modeId,
|
|
325
|
+
availableModes: [
|
|
326
|
+
{
|
|
327
|
+
id: 'interactive',
|
|
328
|
+
name: 'Interactive',
|
|
329
|
+
description: text.acp.modeInteractiveDescription,
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
id: 'yolo',
|
|
333
|
+
name: 'YOLO',
|
|
334
|
+
description: text.acp.modeYoloDescription,
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
};
|
|
338
|
+
}
|
|
303
339
|
function buildCurrentModeUpdateIfChanged(state) {
|
|
304
340
|
const digest = state.modeId;
|
|
305
341
|
if (digest === state.lastModeDigest)
|
|
@@ -326,6 +362,7 @@ function createSessionRuntimeStateFromPersisted(input) {
|
|
|
326
362
|
lastCommandsDigest: null,
|
|
327
363
|
lastConfigDigest: null,
|
|
328
364
|
lastModeDigest: null,
|
|
365
|
+
lastSessionInfoDigest: null,
|
|
329
366
|
permissionPolicy,
|
|
330
367
|
modeId,
|
|
331
368
|
};
|
|
@@ -456,6 +493,15 @@ export function createAcpFormalAgent(deps) {
|
|
|
456
493
|
terminal: false,
|
|
457
494
|
};
|
|
458
495
|
const loadSessionCapability = deps.capabilityPolicy?.loadSession ?? true;
|
|
496
|
+
const promptCapabilities = {
|
|
497
|
+
image: deps.capabilityPolicy?.promptCapabilities?.image ?? false,
|
|
498
|
+
audio: deps.capabilityPolicy?.promptCapabilities?.audio ?? false,
|
|
499
|
+
embeddedContext: deps.capabilityPolicy?.promptCapabilities?.embeddedContext ?? false,
|
|
500
|
+
};
|
|
501
|
+
const mcpCapabilities = {
|
|
502
|
+
http: deps.capabilityPolicy?.mcpCapabilities?.http ?? false,
|
|
503
|
+
sse: deps.capabilityPolicy?.mcpCapabilities?.sse ?? false,
|
|
504
|
+
};
|
|
459
505
|
const sessionPersistencePath = deps.sessionPersistencePath;
|
|
460
506
|
const sessionStorePolicy = {
|
|
461
507
|
maxEntries: deps.sessionStorePolicy?.maxEntries ?? ACP_SESSION_STORE_MAX_ENTRIES,
|
|
@@ -779,6 +825,21 @@ export function createAcpFormalAgent(deps) {
|
|
|
779
825
|
async function emitSessionUpdate(sessionId, update) {
|
|
780
826
|
await deps.conn.sessionUpdate({ sessionId, update });
|
|
781
827
|
}
|
|
828
|
+
async function emitSessionInfoUpdateBestEffort(sessionId) {
|
|
829
|
+
const session = sessions.get(sessionId);
|
|
830
|
+
if (!session)
|
|
831
|
+
return;
|
|
832
|
+
const state = ensureSessionRuntimeState(sessionId);
|
|
833
|
+
const update = buildSessionInfoUpdateIfChanged(session, state);
|
|
834
|
+
if (!update)
|
|
835
|
+
return;
|
|
836
|
+
try {
|
|
837
|
+
await emitSessionUpdate(sessionId, update);
|
|
838
|
+
}
|
|
839
|
+
catch {
|
|
840
|
+
// Best-effort: do not fail the request due to notification delivery issues.
|
|
841
|
+
}
|
|
842
|
+
}
|
|
782
843
|
async function emitRuntimePlanUpdateIfNeeded(params) {
|
|
783
844
|
if (!shouldRefreshPlanForEvent(params.event))
|
|
784
845
|
return;
|
|
@@ -846,14 +907,22 @@ export function createAcpFormalAgent(deps) {
|
|
|
846
907
|
throw new RequestError(-32602, 'Invalid params: protocolVersion is required');
|
|
847
908
|
}
|
|
848
909
|
clientCapabilities = params.clientCapabilities;
|
|
910
|
+
// Protocol version negotiation:
|
|
911
|
+
// - If the client's requested version is supported, return the same version
|
|
912
|
+
// - Otherwise, return the latest version the agent supports
|
|
913
|
+
// Currently, the agent only supports protocol version 1
|
|
914
|
+
const supportedProtocolVersion = PROTOCOL_VERSION;
|
|
915
|
+
const negotiatedVersion = params.protocolVersion <= supportedProtocolVersion
|
|
916
|
+
? params.protocolVersion
|
|
917
|
+
: supportedProtocolVersion;
|
|
849
918
|
return {
|
|
850
|
-
protocolVersion:
|
|
919
|
+
protocolVersion: negotiatedVersion,
|
|
851
920
|
agentInfo: deps.agentInfo,
|
|
852
921
|
authMethods: [],
|
|
853
922
|
agentCapabilities: {
|
|
854
923
|
loadSession: loadSessionCapability,
|
|
855
|
-
promptCapabilities:
|
|
856
|
-
mcpCapabilities:
|
|
924
|
+
promptCapabilities: promptCapabilities,
|
|
925
|
+
mcpCapabilities: mcpCapabilities,
|
|
857
926
|
sessionCapabilities: {},
|
|
858
927
|
},
|
|
859
928
|
};
|
|
@@ -866,9 +935,14 @@ export function createAcpFormalAgent(deps) {
|
|
|
866
935
|
if (!isAbsolutePath(params.cwd)) {
|
|
867
936
|
throw new RequestError(-32602, 'Invalid params: cwd must be an absolute path');
|
|
868
937
|
}
|
|
869
|
-
const session = sessions.create({
|
|
938
|
+
const session = sessions.create({
|
|
939
|
+
cwd: params.cwd,
|
|
940
|
+
mcpServers: params.mcpServers ?? [],
|
|
941
|
+
title: deriveSessionTitleFromCwd(params.cwd),
|
|
942
|
+
});
|
|
870
943
|
await persistSessionsBestEffort();
|
|
871
944
|
const runtimeState = ensureSessionRuntimeState(session.id);
|
|
945
|
+
await emitSessionInfoUpdateBestEffort(session.id);
|
|
872
946
|
// Restore session state on creation
|
|
873
947
|
const commandsUpdate = buildAvailableCommandsUpdateIfChanged(runtimeState);
|
|
874
948
|
if (commandsUpdate)
|
|
@@ -894,6 +968,7 @@ export function createAcpFormalAgent(deps) {
|
|
|
894
968
|
return {
|
|
895
969
|
sessionId: session.id,
|
|
896
970
|
configOptions: buildConfigOptions(runtimeState),
|
|
971
|
+
modes: buildModesState(runtimeState.modeId),
|
|
897
972
|
...(sessionMeta ? { _meta: sessionMeta } : {}),
|
|
898
973
|
};
|
|
899
974
|
},
|
|
@@ -902,8 +977,18 @@ export function createAcpFormalAgent(deps) {
|
|
|
902
977
|
throw new RequestError(-32601, '"Method not found": session/load');
|
|
903
978
|
}
|
|
904
979
|
await loadSessionInternal(params);
|
|
905
|
-
|
|
980
|
+
let session = sessions.get(params.sessionId);
|
|
906
981
|
const runtimeState = ensureSessionRuntimeState(session.id);
|
|
982
|
+
if (typeof session.title !== 'string' || !session.title.trim()) {
|
|
983
|
+
session =
|
|
984
|
+
sessions.update(session.id, (current) => ({
|
|
985
|
+
...current,
|
|
986
|
+
title: deriveSessionTitleFromCwd(current.cwd),
|
|
987
|
+
})) ?? session;
|
|
988
|
+
await persistSessionsBestEffort();
|
|
989
|
+
}
|
|
990
|
+
runtimeState.lastSessionInfoDigest = null;
|
|
991
|
+
await emitSessionInfoUpdateBestEffort(session.id);
|
|
907
992
|
// Restore plan state if session was running a task
|
|
908
993
|
if (session.taskId && session.cwd) {
|
|
909
994
|
await emitRuntimePlanUpdateIfNeeded({
|
|
@@ -932,6 +1017,7 @@ export function createAcpFormalAgent(deps) {
|
|
|
932
1017
|
}
|
|
933
1018
|
const response = {
|
|
934
1019
|
configOptions: buildConfigOptions(runtimeState),
|
|
1020
|
+
modes: buildModesState(runtimeState.modeId),
|
|
935
1021
|
};
|
|
936
1022
|
if (deps.checkpointReader) {
|
|
937
1023
|
const startedAt = Date.now();
|
|
@@ -1019,6 +1105,7 @@ export function createAcpFormalAgent(deps) {
|
|
|
1019
1105
|
}
|
|
1020
1106
|
sessions.update(params.sessionId, (current) => ({ ...current }));
|
|
1021
1107
|
await persistSessionsBestEffort();
|
|
1108
|
+
await emitSessionInfoUpdateBestEffort(params.sessionId);
|
|
1022
1109
|
const update = buildConfigOptionUpdateIfChanged(runtimeState);
|
|
1023
1110
|
if (update) {
|
|
1024
1111
|
await emitSessionUpdate(params.sessionId, update);
|
|
@@ -1029,6 +1116,26 @@ export function createAcpFormalAgent(deps) {
|
|
|
1029
1116
|
}
|
|
1030
1117
|
return { configOptions: buildConfigOptions(runtimeState) };
|
|
1031
1118
|
},
|
|
1119
|
+
async setSessionMode(params) {
|
|
1120
|
+
await hydrateSessionsOnce();
|
|
1121
|
+
if (!sessions.get(params.sessionId)) {
|
|
1122
|
+
throw new RequestError(-32004, `Session not found: ${params.sessionId}`);
|
|
1123
|
+
}
|
|
1124
|
+
const runtimeState = ensureSessionRuntimeState(params.sessionId);
|
|
1125
|
+
if (!isSessionModeId(params.modeId)) {
|
|
1126
|
+
throw new RequestError(-32602, `Invalid params: unsupported modeId "${params.modeId}"`);
|
|
1127
|
+
}
|
|
1128
|
+
runtimeState.modeId = params.modeId;
|
|
1129
|
+
sessions.update(params.sessionId, (current) => ({ ...current }));
|
|
1130
|
+
await persistSessionsBestEffort();
|
|
1131
|
+
await emitSessionInfoUpdateBestEffort(params.sessionId);
|
|
1132
|
+
// Send mode update notification
|
|
1133
|
+
const modeUpdate = buildCurrentModeUpdateIfChanged(runtimeState);
|
|
1134
|
+
if (modeUpdate) {
|
|
1135
|
+
await emitSessionUpdate(params.sessionId, modeUpdate);
|
|
1136
|
+
}
|
|
1137
|
+
return {};
|
|
1138
|
+
},
|
|
1032
1139
|
async prompt(params) {
|
|
1033
1140
|
await hydrateSessionsOnce();
|
|
1034
1141
|
const session = sessions.get(params.sessionId);
|
|
@@ -1039,12 +1146,20 @@ export function createAcpFormalAgent(deps) {
|
|
|
1039
1146
|
const fsCaps = caps.fs;
|
|
1040
1147
|
const clientExecutionReady = caps.terminal === true && Boolean(fsCaps?.readTextFile) && Boolean(fsCaps?.writeTextFile);
|
|
1041
1148
|
const effectiveExecutionBinding = executionBinding === 'client' && !clientExecutionReady ? 'local' : executionBinding;
|
|
1042
|
-
const promptText = extractTextFromPrompt(params.prompt,
|
|
1149
|
+
const promptText = extractTextFromPrompt(params.prompt, promptCapabilities);
|
|
1043
1150
|
const runtimeState = ensureSessionRuntimeState(params.sessionId);
|
|
1151
|
+
// Check for cancellation before starting processing
|
|
1152
|
+
if (sessions.get(params.sessionId)?.cancelRequested === true) {
|
|
1153
|
+
return { stopReason: 'cancelled' };
|
|
1154
|
+
}
|
|
1044
1155
|
sessions.update(params.sessionId, (current) => {
|
|
1156
|
+
const title = typeof current.title === 'string' && current.title.trim()
|
|
1157
|
+
? current.title
|
|
1158
|
+
: deriveSessionTitleFromCwd(current.cwd);
|
|
1045
1159
|
return {
|
|
1046
1160
|
...current,
|
|
1047
1161
|
cancelRequested: false,
|
|
1162
|
+
title,
|
|
1048
1163
|
history: [
|
|
1049
1164
|
...current.history,
|
|
1050
1165
|
{ role: 'user', content: params.prompt },
|
|
@@ -1052,6 +1167,7 @@ export function createAcpFormalAgent(deps) {
|
|
|
1052
1167
|
};
|
|
1053
1168
|
});
|
|
1054
1169
|
await persistSessionsBestEffort();
|
|
1170
|
+
await emitSessionInfoUpdateBestEffort(params.sessionId);
|
|
1055
1171
|
const configUpdate = buildConfigOptionUpdateIfChanged(runtimeState);
|
|
1056
1172
|
if (configUpdate) {
|
|
1057
1173
|
await emitSessionUpdate(params.sessionId, configUpdate);
|
|
@@ -1083,16 +1199,23 @@ export function createAcpFormalAgent(deps) {
|
|
|
1083
1199
|
],
|
|
1084
1200
|
}));
|
|
1085
1201
|
await persistSessionsBestEffort();
|
|
1202
|
+
await emitSessionInfoUpdateBestEffort(params.sessionId);
|
|
1086
1203
|
return { stopReason: 'end_turn' };
|
|
1087
1204
|
}
|
|
1088
1205
|
}
|
|
1089
|
-
|
|
1206
|
+
// Check for cancellation again before creating task
|
|
1207
|
+
if (sessions.get(params.sessionId)?.cancelRequested === true) {
|
|
1208
|
+
return { stopReason: 'cancelled' };
|
|
1209
|
+
}
|
|
1210
|
+
const pendingUpdates = [];
|
|
1211
|
+
const executionRequest = buildCanonicalExecutionRequest({
|
|
1090
1212
|
capability: 'patch',
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1213
|
+
instruction: promptText,
|
|
1214
|
+
checkpointSessionId: params.sessionId,
|
|
1215
|
+
repoPath: session.cwd,
|
|
1216
|
+
});
|
|
1217
|
+
const { task, signal } = await deps.facade.createTask({
|
|
1218
|
+
...executionRequest,
|
|
1096
1219
|
commandRunner: effectiveExecutionBinding === 'client'
|
|
1097
1220
|
? createAcpCommandRunner({ conn: deps.conn, sessionId: params.sessionId })
|
|
1098
1221
|
: undefined,
|
|
@@ -1109,14 +1232,18 @@ export function createAcpFormalAgent(deps) {
|
|
|
1109
1232
|
authorizationMode: 'blocking',
|
|
1110
1233
|
onEvent: (event) => {
|
|
1111
1234
|
for (const update of loopEventToSessionUpdates(event, runtimeState)) {
|
|
1112
|
-
|
|
1235
|
+
pendingUpdates.push(emitSessionUpdate(params.sessionId, update).catch(() => {
|
|
1236
|
+
// Ignore errors in session update notifications
|
|
1237
|
+
}));
|
|
1113
1238
|
}
|
|
1114
|
-
|
|
1239
|
+
pendingUpdates.push(emitRuntimePlanUpdateIfNeeded({
|
|
1115
1240
|
sessionId: params.sessionId,
|
|
1116
1241
|
repoPath: session.cwd,
|
|
1117
1242
|
event,
|
|
1118
1243
|
state: runtimeState,
|
|
1119
|
-
})
|
|
1244
|
+
}).catch(() => {
|
|
1245
|
+
// Ignore errors in plan update notifications
|
|
1246
|
+
}));
|
|
1120
1247
|
},
|
|
1121
1248
|
});
|
|
1122
1249
|
sessions.update(params.sessionId, (current) => ({ ...current, taskId: task.id }));
|
|
@@ -1181,6 +1308,17 @@ export function createAcpFormalAgent(deps) {
|
|
|
1181
1308
|
],
|
|
1182
1309
|
}));
|
|
1183
1310
|
await persistSessionsBestEffort();
|
|
1311
|
+
const latestSession = sessions.get(params.sessionId);
|
|
1312
|
+
if (latestSession) {
|
|
1313
|
+
const sessionInfoUpdate = buildSessionInfoUpdateIfChanged(latestSession, runtimeState);
|
|
1314
|
+
if (sessionInfoUpdate) {
|
|
1315
|
+
pendingUpdates.push(emitSessionUpdate(params.sessionId, sessionInfoUpdate).catch(() => {
|
|
1316
|
+
// Ignore errors in session update notifications
|
|
1317
|
+
}));
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
// Wait for all pending session updates to be sent before responding
|
|
1321
|
+
await Promise.all(pendingUpdates);
|
|
1184
1322
|
return { stopReason };
|
|
1185
1323
|
},
|
|
1186
1324
|
async cancel(params) {
|
|
@@ -1188,11 +1326,16 @@ export function createAcpFormalAgent(deps) {
|
|
|
1188
1326
|
const session = sessions.get(params.sessionId);
|
|
1189
1327
|
if (!session)
|
|
1190
1328
|
return;
|
|
1329
|
+
// Mark the session as cancelled
|
|
1191
1330
|
sessions.update(params.sessionId, (current) => ({ ...current, cancelRequested: true }));
|
|
1192
1331
|
await persistSessionsBestEffort();
|
|
1332
|
+
await emitSessionInfoUpdateBestEffort(params.sessionId);
|
|
1333
|
+
// If a task is running, cancel it
|
|
1193
1334
|
if (session.taskId) {
|
|
1194
1335
|
await deps.facade.cancelTask(session.taskId);
|
|
1195
1336
|
}
|
|
1337
|
+
// Note: The prompt method will check the cancelRequested flag and return
|
|
1338
|
+
// StopReason::Cancelled as required by the protocol
|
|
1196
1339
|
},
|
|
1197
1340
|
extMethod: async () => ({}),
|
|
1198
1341
|
extNotification: async () => { },
|