@cjhyy/code-shell-core 0.9.4 → 0.9.6
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/credentials/access.d.ts +9 -1
- package/dist/credentials/access.js +15 -4
- package/dist/credentials/store.d.ts +11 -0
- package/dist/credentials/store.js +44 -9
- package/dist/credentials/types.d.ts +4 -0
- package/dist/credentials/types.js +4 -0
- package/dist/credentials/use-credential-tool.js +12 -2
- package/dist/engine/engine-workspace-authority.js +10 -3
- package/dist/engine/engine.d.ts +3 -0
- package/dist/engine/engine.js +38 -6
- package/dist/engine/prompt-cache-diagnostics.js +10 -1
- package/dist/engine/run-goal.js +7 -3
- package/dist/engine/run-types.d.ts +16 -0
- package/dist/engine/run-workspace.js +5 -21
- package/dist/engine/turn-loop.d.ts +4 -4
- package/dist/engine/turn-loop.js +29 -9
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/links/index.d.ts +1 -0
- package/dist/links/index.js +1 -0
- package/dist/links/link-action-tool.d.ts +2 -1
- package/dist/links/link-action-tool.js +80 -44
- package/dist/links/status.d.ts +53 -0
- package/dist/links/status.js +175 -0
- package/dist/llm/prompt-cache.d.ts +35 -3
- package/dist/llm/prompt-cache.js +63 -3
- package/dist/llm/providers/openai.d.ts +3 -0
- package/dist/llm/providers/openai.js +57 -14
- package/dist/protocol/background-result-wakeup.d.ts +8 -1
- package/dist/protocol/background-result-wakeup.js +76 -38
- package/dist/protocol/chat-session-manager.d.ts +7 -1
- package/dist/protocol/chat-session-manager.js +44 -8
- package/dist/protocol/chat-session.d.ts +2 -0
- package/dist/protocol/chat-session.js +4 -1
- package/dist/protocol/server.d.ts +3 -0
- package/dist/protocol/server.js +148 -43
- package/dist/protocol/session-message-result.d.ts +12 -0
- package/dist/protocol/session-message-result.js +42 -0
- package/dist/protocol/session-message-workspace.d.ts +21 -0
- package/dist/protocol/session-message-workspace.js +57 -0
- package/dist/protocol/types.d.ts +2 -0
- package/dist/session/session-message.d.ts +17 -2
- package/dist/tool-system/browser-bridge.d.ts +18 -0
- package/dist/tool-system/builtin/agent.js +23 -9
- package/dist/tool-system/builtin/browser-tools.js +18 -1
- package/dist/tool-system/builtin/index.js +3 -3
- package/dist/tool-system/builtin/send-message-to-session.js +19 -3
- package/package.json +1 -1
|
@@ -16,6 +16,11 @@ export interface CredentialMetadata {
|
|
|
16
16
|
}
|
|
17
17
|
export interface CredentialAccess {
|
|
18
18
|
listMasked(cwd: string | undefined, scope: CredentialAccessScope): CredentialMetadata[];
|
|
19
|
+
/** Optional read diagnostics. False means the list may be incomplete, not that it is empty. */
|
|
20
|
+
listMaskedWithStatus?(cwd: string | undefined, scope: CredentialAccessScope): {
|
|
21
|
+
credentials: CredentialMetadata[];
|
|
22
|
+
readable: boolean;
|
|
23
|
+
};
|
|
19
24
|
resolveMeta(cwd: string | undefined, id: string, scope: CredentialAccessScope): CredentialMetadata | undefined;
|
|
20
25
|
envExposures(cwd: string | undefined, scope: CredentialAccessScope): Record<string, string>;
|
|
21
26
|
/** Subscribe to host credential snapshot changes (desktop worker uses this to cancel in-flight Links). */
|
|
@@ -48,6 +53,9 @@ export interface CredentialSnapshotEntry {
|
|
|
48
53
|
cwd?: string;
|
|
49
54
|
full: CredentialMetadata[];
|
|
50
55
|
project: CredentialMetadata[];
|
|
56
|
+
/** Older hosts omit these; their credential-store readability is unknown. */
|
|
57
|
+
readableFull?: boolean;
|
|
58
|
+
readableProject?: boolean;
|
|
51
59
|
envFull: Record<string, string>;
|
|
52
60
|
envProject: Record<string, string>;
|
|
53
61
|
}
|
|
@@ -59,7 +67,7 @@ export declare function setDefaultCredentialAccess(access: CredentialAccess | nu
|
|
|
59
67
|
export declare function getCredentialAccess(): CredentialAccess;
|
|
60
68
|
export declare function createIpcCredentialAccess(transport: Pick<Transport, "send" | "onMessage">): CredentialAccess;
|
|
61
69
|
export declare function credentialAccessScope(scope: SettingsScope | undefined): CredentialAccessScope;
|
|
62
|
-
export
|
|
70
|
+
export { isCredentialSecretAvailable } from "./types.js";
|
|
63
71
|
export declare const localCredentialAccess: CredentialAccess;
|
|
64
72
|
export declare function materializeCookieSecret(credentialId: string, secret: string): {
|
|
65
73
|
cookiesFile: string;
|
|
@@ -2,7 +2,7 @@ import { existsSync, readdirSync, rmSync, statSync, writeFileSync } from "node:f
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { credentialAllowsEnvExposure, credentialSecretHint, } from "./types.js";
|
|
5
|
+
import { credentialAllowsEnvExposure, credentialSecretHint, isCredentialSecretAvailable, } from "./types.js";
|
|
6
6
|
import { CredentialStore } from "./store.js";
|
|
7
7
|
import { formatNetscapeCookies, parseCookieJar } from "./cookie-jar.js";
|
|
8
8
|
import { parseOAuthCredentialSecret, isBrowserOAuthLinkCredential, resolveLinkCredentialAccessToken, shouldRefreshOAuthCredential, summarizeOAuthCredentialSecret, } from "./oauth.js";
|
|
@@ -77,6 +77,15 @@ export function createIpcCredentialAccess(transport) {
|
|
|
77
77
|
return [];
|
|
78
78
|
return scope === "project" ? cloneMetadata(entry.project) : cloneMetadata(entry.full);
|
|
79
79
|
},
|
|
80
|
+
listMaskedWithStatus(cwd, scope) {
|
|
81
|
+
const entry = entryFor(cwd);
|
|
82
|
+
if (!entry)
|
|
83
|
+
return { credentials: [], readable: false };
|
|
84
|
+
return {
|
|
85
|
+
credentials: cloneMetadata(scope === "project" ? entry.project : entry.full),
|
|
86
|
+
readable: (scope === "project" ? entry.readableProject : entry.readableFull) === true,
|
|
87
|
+
};
|
|
88
|
+
},
|
|
80
89
|
resolveMeta(cwd, id, scope) {
|
|
81
90
|
const entry = entryFor(cwd);
|
|
82
91
|
if (!entry)
|
|
@@ -122,9 +131,7 @@ export function createIpcCredentialAccess(transport) {
|
|
|
122
131
|
export function credentialAccessScope(scope) {
|
|
123
132
|
return scope === "full" || scope === undefined ? "full" : "project";
|
|
124
133
|
}
|
|
125
|
-
export
|
|
126
|
-
return typeof secret === "string" && secret.length > 0 && !secret.startsWith("enc:");
|
|
127
|
-
}
|
|
134
|
+
export { isCredentialSecretAvailable } from "./types.js";
|
|
128
135
|
function toMetadata(cred) {
|
|
129
136
|
const secret = cred.secret;
|
|
130
137
|
const available = isCredentialSecretAvailable(secret);
|
|
@@ -149,6 +156,10 @@ export const localCredentialAccess = {
|
|
|
149
156
|
listMasked(cwd, scope) {
|
|
150
157
|
return storeFor(cwd).list(scope).map(toMetadata);
|
|
151
158
|
},
|
|
159
|
+
listMaskedWithStatus(cwd, scope) {
|
|
160
|
+
const result = storeFor(cwd).listWithStatus(scope);
|
|
161
|
+
return { credentials: result.credentials.map(toMetadata), readable: result.readable };
|
|
162
|
+
},
|
|
152
163
|
resolveMeta(cwd, id, scope) {
|
|
153
164
|
const cred = storeFor(cwd).resolve(id, scope);
|
|
154
165
|
return cred ? toMetadata(cred) : undefined;
|
|
@@ -70,6 +70,12 @@ export declare class CredentialStore {
|
|
|
70
70
|
* id 不存在则 no-op。
|
|
71
71
|
*/
|
|
72
72
|
patch(scope: CredentialScope, id: string, fields: Partial<Pick<Credential, "label" | "exposeAsEnv" | "autoUseByAI" | "autoInjectByAI" | "meta">>): void;
|
|
73
|
+
/**
|
|
74
|
+
* Update an existing record while holding the store lock. The callback sees
|
|
75
|
+
* the latest decrypted record and may return undefined to decline the write.
|
|
76
|
+
* Unlike save(), this never recreates a deleted credential.
|
|
77
|
+
*/
|
|
78
|
+
updateExisting(scope: CredentialScope, id: string, update: (current: Credential) => Credential | undefined): boolean;
|
|
73
79
|
remove(scope: CredentialScope, id: string): void;
|
|
74
80
|
/**
|
|
75
81
|
* List credentials visible to an engine of the given settings scope.
|
|
@@ -80,6 +86,11 @@ export declare class CredentialStore {
|
|
|
80
86
|
* same host-isolation contract as {@link envExposures} and top-level env.
|
|
81
87
|
*/
|
|
82
88
|
list(scope?: "full" | "project"): Credential[];
|
|
89
|
+
/** Preserve read failures so diagnostics can distinguish unknown state from an empty store. */
|
|
90
|
+
listWithStatus(scope?: "full" | "project"): {
|
|
91
|
+
credentials: Credential[];
|
|
92
|
+
readable: boolean;
|
|
93
|
+
};
|
|
83
94
|
resolve(id: string, scope?: "full" | "project"): Credential | undefined;
|
|
84
95
|
/**
|
|
85
96
|
* Credentials flagged "expose as env var" → a `{ ENV_NAME: secret }` map for
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { credentialAllowsEnvExposure, credentialSecretHint, } from "./types.js";
|
|
4
|
+
import { credentialAllowsEnvExposure, credentialSecretHint, isCredentialSecretAvailable, } from "./types.js";
|
|
5
5
|
import { getDefaultCredentialCipher } from "./cipher.js";
|
|
6
6
|
import { logger } from "../logging/logger.js";
|
|
7
7
|
import { summarizeOAuthCredentialSecret } from "./oauth.js";
|
|
@@ -329,6 +329,34 @@ export class CredentialStore {
|
|
|
329
329
|
return true;
|
|
330
330
|
});
|
|
331
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Update an existing record while holding the store lock. The callback sees
|
|
334
|
+
* the latest decrypted record and may return undefined to decline the write.
|
|
335
|
+
* Unlike save(), this never recreates a deleted credential.
|
|
336
|
+
*/
|
|
337
|
+
updateExisting(scope, id, update) {
|
|
338
|
+
if (typeof id !== "string" || !id || id.length > MAX_CREDENTIAL_ID_CHARS || id.includes("\0")) {
|
|
339
|
+
throw new Error("invalid credential id");
|
|
340
|
+
}
|
|
341
|
+
let changed = false;
|
|
342
|
+
this.mutate(scope, (file) => {
|
|
343
|
+
const idx = file.credentials.findIndex((credential) => credential.id === id);
|
|
344
|
+
if (idx < 0)
|
|
345
|
+
return false;
|
|
346
|
+
const next = update(normalizeCredential(file.credentials[idx], true));
|
|
347
|
+
if (!next)
|
|
348
|
+
return false;
|
|
349
|
+
const normalized = normalizeCredential(next, true);
|
|
350
|
+
if (normalized.id !== id)
|
|
351
|
+
throw new Error("credential update cannot change id");
|
|
352
|
+
file.credentials[idx] = credentialAllowsEnvExposure(normalized.type)
|
|
353
|
+
? normalized
|
|
354
|
+
: { ...normalized, exposeAsEnv: undefined };
|
|
355
|
+
changed = true;
|
|
356
|
+
return true;
|
|
357
|
+
});
|
|
358
|
+
return changed;
|
|
359
|
+
}
|
|
332
360
|
remove(scope, id) {
|
|
333
361
|
if (typeof id !== "string" || !id || id.length > MAX_CREDENTIAL_ID_CHARS || id.includes("\0")) {
|
|
334
362
|
throw new Error("invalid credential id");
|
|
@@ -350,14 +378,20 @@ export class CredentialStore {
|
|
|
350
378
|
* same host-isolation contract as {@link envExposures} and top-level env.
|
|
351
379
|
*/
|
|
352
380
|
list(scope = "full") {
|
|
381
|
+
return this.listWithStatus(scope).credentials;
|
|
382
|
+
}
|
|
383
|
+
/** Preserve read failures so diagnostics can distinguish unknown state from an empty store. */
|
|
384
|
+
listWithStatus(scope = "full") {
|
|
353
385
|
const byId = new Map();
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
386
|
+
let readable = true;
|
|
387
|
+
const scopes = scope === "full" ? ["user", "project"] : ["project"];
|
|
388
|
+
for (const layer of scopes) {
|
|
389
|
+
const result = this.readGuarded(layer);
|
|
390
|
+
readable = readable && result.readable;
|
|
391
|
+
for (const credential of result.file.credentials)
|
|
392
|
+
byId.set(credential.id, credential);
|
|
357
393
|
}
|
|
358
|
-
|
|
359
|
-
byId.set(c.id, c); // project wins
|
|
360
|
-
return [...byId.values()];
|
|
394
|
+
return { credentials: [...byId.values()], readable };
|
|
361
395
|
}
|
|
362
396
|
resolve(id, scope = "full") {
|
|
363
397
|
return this.list(scope).find((c) => c.id === id);
|
|
@@ -397,11 +431,12 @@ export class CredentialStore {
|
|
|
397
431
|
listMasked(scope = "full") {
|
|
398
432
|
return this.list(scope).map((c) => {
|
|
399
433
|
const { secret, ...rest } = c;
|
|
434
|
+
const available = isCredentialSecretAvailable(secret);
|
|
400
435
|
return {
|
|
401
436
|
...rest,
|
|
402
437
|
...(credentialAllowsEnvExposure(c.type) ? {} : { exposeAsEnv: undefined }),
|
|
403
|
-
hasSecret:
|
|
404
|
-
secretHint: credentialSecretHint(c.type, secret),
|
|
438
|
+
hasSecret: available,
|
|
439
|
+
secretHint: available ? credentialSecretHint(c.type, secret) : undefined,
|
|
405
440
|
...(c.type === "oauth" || isBrowserOAuthLinkCredential(c)
|
|
406
441
|
? { oauthStatus: summarizeOAuthCredentialSecret(secret) }
|
|
407
442
|
: {}),
|
|
@@ -10,6 +10,8 @@ export type CredentialType = "token" | "link" | "cookie" | "oauth";
|
|
|
10
10
|
* values and must remain behind the credential resolver boundary.
|
|
11
11
|
*/
|
|
12
12
|
export declare function credentialAllowsEnvExposure(type: CredentialType): boolean;
|
|
13
|
+
/** An unreadable encrypted value is preserved on disk, but is not a usable secret. */
|
|
14
|
+
export declare function isCredentialSecretAvailable(secret: string | undefined): secret is string;
|
|
13
15
|
/** Build a renderer-safe hint without deriving bytes from structured JSON. */
|
|
14
16
|
export declare function credentialSecretHint(type: CredentialType, secret: string | undefined): string | undefined;
|
|
15
17
|
export interface OAuthCredentialSecret {
|
|
@@ -112,6 +114,8 @@ export interface Credential {
|
|
|
112
114
|
domain?: string;
|
|
113
115
|
scope?: "domain" | "all";
|
|
114
116
|
switchMode?: "clear" | "merge";
|
|
117
|
+
/** Opt in to writing browser-side cookie rotations back to this saved credential. */
|
|
118
|
+
autoRefreshFromBrowser?: boolean;
|
|
115
119
|
/** Link provider id for credentials owned by a local/server app connection. */
|
|
116
120
|
linkProvider?: string;
|
|
117
121
|
/** Provider-specific connection method, e.g. fine-grained-pat or github-app. */
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
export function credentialAllowsEnvExposure(type) {
|
|
7
7
|
return type === "token" || type === "link";
|
|
8
8
|
}
|
|
9
|
+
/** An unreadable encrypted value is preserved on disk, but is not a usable secret. */
|
|
10
|
+
export function isCredentialSecretAvailable(secret) {
|
|
11
|
+
return typeof secret === "string" && secret.length > 0 && !secret.startsWith("enc:");
|
|
12
|
+
}
|
|
9
13
|
/** Build a renderer-safe hint without deriving bytes from structured JSON. */
|
|
10
14
|
export function credentialSecretHint(type, secret) {
|
|
11
15
|
if (!secret)
|
|
@@ -23,7 +23,10 @@ const BASE_DESCRIPTION = "Use a stored credential (token / API key / login cooki
|
|
|
23
23
|
"or manually exporting credentials. Before `--cookies-from-browser` or retrying an " +
|
|
24
24
|
"authentication failure, check stored credentials here. If `Currently available` names a " +
|
|
25
25
|
"matching id, fetch it directly; otherwise call with NO arguments first to list available " +
|
|
26
|
-
"credentials (id + label + type), then call again with `id`.
|
|
26
|
+
"credentials (id + label + type), then call again with `id`. Provider-owned Link connections " +
|
|
27
|
+
"and existing CLI sessions are not listed here. Call LinkAction with provider only to check " +
|
|
28
|
+
"those before concluding " +
|
|
29
|
+
"a service is disconnected or asking the user to log in. Exposable token credentials return their secret " +
|
|
27
30
|
"value; cookie credentials are materialized to a temporary Netscape cookies.txt file " +
|
|
28
31
|
"(use it as `yt-dlp --cookies <cookiesFile>` / `curl -b <cookiesFile>`). " +
|
|
29
32
|
"Each use is gated by a quick user approval unless auto-approve is on.";
|
|
@@ -125,7 +128,14 @@ export async function useCredentialTool(args, ctx) {
|
|
|
125
128
|
.listMasked(cwd, scope)
|
|
126
129
|
.filter(isAgentExposableCredential)
|
|
127
130
|
.map((c) => ({ id: c.id, label: c.label, type: c.type }));
|
|
128
|
-
return json({
|
|
131
|
+
return json({
|
|
132
|
+
kind: "list",
|
|
133
|
+
credentials,
|
|
134
|
+
notice: "This list excludes provider-owned Link credentials and CLI login sessions. " +
|
|
135
|
+
'Call LinkAction with provider only, for example LinkAction({provider: "github"}), ' +
|
|
136
|
+
"to check saved connections and current CLI authentication; " +
|
|
137
|
+
"an absent credential here does not mean the service is signed out.",
|
|
138
|
+
});
|
|
129
139
|
}
|
|
130
140
|
const cred = access.resolveMeta(cwd, id, scope);
|
|
131
141
|
if (!cred) {
|
|
@@ -104,7 +104,8 @@ export function createAuthorizedSessionMessageService(options) {
|
|
|
104
104
|
const targets = catalog.filter((target) => target.sessionId !== sourceSessionId);
|
|
105
105
|
return {
|
|
106
106
|
targets,
|
|
107
|
-
send: async ({ targetSessionId, message }) => {
|
|
107
|
+
send: async ({ targetSessionId, message, signal }) => {
|
|
108
|
+
signal?.throwIfAborted();
|
|
108
109
|
const target = targets.find((candidate) => candidate.sessionId === targetSessionId);
|
|
109
110
|
if (!target)
|
|
110
111
|
throw new Error("target Session is not in the host-authorized project list");
|
|
@@ -112,8 +113,14 @@ export function createAuthorizedSessionMessageService(options) {
|
|
|
112
113
|
throw new Error("message is required");
|
|
113
114
|
if (message.length > 48_000)
|
|
114
115
|
throw new Error("message exceeds 48000 characters");
|
|
115
|
-
await router({
|
|
116
|
-
|
|
116
|
+
const receipt = await router({
|
|
117
|
+
sourceSessionId,
|
|
118
|
+
target,
|
|
119
|
+
message,
|
|
120
|
+
catalog,
|
|
121
|
+
...(signal ? { signal } : {}),
|
|
122
|
+
});
|
|
123
|
+
return receipt ? { ...target, receipt } : target;
|
|
117
124
|
},
|
|
118
125
|
};
|
|
119
126
|
}
|
package/dist/engine/engine.d.ts
CHANGED
|
@@ -158,6 +158,8 @@ export declare class Engine {
|
|
|
158
158
|
* running goal's turn/budget ceilings mid-run (TODO 3.1). Null when idle.
|
|
159
159
|
*/
|
|
160
160
|
private activeTurnLoop;
|
|
161
|
+
/** Frozen behavior-profile ceiling for the live run's Goal controls. */
|
|
162
|
+
private activeProfileMaxTurns;
|
|
161
163
|
/**
|
|
162
164
|
* The goal-stop hook of the in-flight goal run, exposed so clearGoal() can
|
|
163
165
|
* unregister it mid-run (the closure holds the now-cleared goal and would
|
|
@@ -459,6 +461,7 @@ export declare class Engine {
|
|
|
459
461
|
* published through the `publishGoalJudgeContext` callback.
|
|
460
462
|
*/
|
|
461
463
|
private buildTurnLoop;
|
|
464
|
+
private resolveRunMaxTurns;
|
|
462
465
|
private buildSummarizeFn;
|
|
463
466
|
private resolveAuxClient;
|
|
464
467
|
private runMemoryPipeline;
|
package/dist/engine/engine.js
CHANGED
|
@@ -232,6 +232,8 @@ export class Engine {
|
|
|
232
232
|
* running goal's turn/budget ceilings mid-run (TODO 3.1). Null when idle.
|
|
233
233
|
*/
|
|
234
234
|
activeTurnLoop = null;
|
|
235
|
+
/** Frozen behavior-profile ceiling for the live run's Goal controls. */
|
|
236
|
+
activeProfileMaxTurns;
|
|
235
237
|
/**
|
|
236
238
|
* The goal-stop hook of the in-flight goal run, exposed so clearGoal() can
|
|
237
239
|
* unregister it mid-run (the closure holds the now-cleared goal and would
|
|
@@ -1251,6 +1253,11 @@ export class Engine {
|
|
|
1251
1253
|
task,
|
|
1252
1254
|
cwd,
|
|
1253
1255
|
options,
|
|
1256
|
+
profileMaxTurns: typeof profile?.maxTurns === "number" &&
|
|
1257
|
+
Number.isSafeInteger(profile.maxTurns) &&
|
|
1258
|
+
profile.maxTurns > 0
|
|
1259
|
+
? profile.maxTurns
|
|
1260
|
+
: undefined,
|
|
1254
1261
|
toolCtx,
|
|
1255
1262
|
toolExecutor,
|
|
1256
1263
|
contextManager,
|
|
@@ -1285,8 +1292,10 @@ export class Engine {
|
|
|
1285
1292
|
this.activeRuntimeGoal = null;
|
|
1286
1293
|
this.activePersistedRunGoal = null;
|
|
1287
1294
|
}
|
|
1288
|
-
if (this.activeTurnLoop === turnLoop)
|
|
1295
|
+
if (this.activeTurnLoop === turnLoop) {
|
|
1289
1296
|
this.activeTurnLoop = null;
|
|
1297
|
+
this.activeProfileMaxTurns = undefined;
|
|
1298
|
+
}
|
|
1290
1299
|
if (this.activeRunSession === session)
|
|
1291
1300
|
this.activeRunSession = null;
|
|
1292
1301
|
// Run-scoped too: this handler is re-registered every run(), so it must be
|
|
@@ -1714,7 +1723,7 @@ export class Engine {
|
|
|
1714
1723
|
* phases consume cross back out.
|
|
1715
1724
|
*/
|
|
1716
1725
|
async wireRunLoop(args) {
|
|
1717
|
-
const { session, sid, task, cwd, options, toolCtx, toolExecutor, contextManager, llmClient, auxSummaryClient, fullSystemPrompt, toolDefs, claimClientMessageId, releaseClientMessageId, freshImageMessage, dynamicContextMsg, } = args;
|
|
1726
|
+
const { session, sid, task, cwd, options, profileMaxTurns, toolCtx, toolExecutor, contextManager, llmClient, auxSummaryClient, fullSystemPrompt, toolDefs, claimClientMessageId, releaseClientMessageId, freshImageMessage, dynamicContextMsg, } = args;
|
|
1718
1727
|
// eslint-disable-next-line prefer-const
|
|
1719
1728
|
let turnLoop;
|
|
1720
1729
|
const accounting = createRunUsageAccounting({
|
|
@@ -1827,6 +1836,7 @@ export class Engine {
|
|
|
1827
1836
|
toolDefs,
|
|
1828
1837
|
sid,
|
|
1829
1838
|
options,
|
|
1839
|
+
profileMaxTurns,
|
|
1830
1840
|
cwd,
|
|
1831
1841
|
claimClientMessageId,
|
|
1832
1842
|
releaseClientMessageId,
|
|
@@ -1846,8 +1856,10 @@ export class Engine {
|
|
|
1846
1856
|
toolCtx.recordBilledUsage = recordExternalBilledUsage;
|
|
1847
1857
|
// Expose this run's loop for mid-run extension (TODO 3.1). Top-level only —
|
|
1848
1858
|
// a sub-agent's loop is its own concern and isn't user-extendable.
|
|
1849
|
-
if (this.config.isSubAgent !== true)
|
|
1859
|
+
if (this.config.isSubAgent !== true) {
|
|
1850
1860
|
this.activeTurnLoop = turnLoop;
|
|
1861
|
+
this.activeProfileMaxTurns = profileMaxTurns;
|
|
1862
|
+
}
|
|
1851
1863
|
// Expose this run's session bundle so a mid-run clearGoal() wipes the goal
|
|
1852
1864
|
// on the very instance this loop keeps saving (see field doc). Top-level
|
|
1853
1865
|
// only — sub-agents don't carry user-clearable persistent goals.
|
|
@@ -2082,7 +2094,7 @@ export class Engine {
|
|
|
2082
2094
|
* published through the `publishGoalJudgeContext` callback.
|
|
2083
2095
|
*/
|
|
2084
2096
|
buildTurnLoop(args) {
|
|
2085
|
-
const { modelFacade, toolExecutor, contextManager, session, fullSystemPrompt, toolDefs, sid, options, cwd, claimClientMessageId, releaseClientMessageId, toolCtx, persistedRunGoal, goalHookHandler, normalizedGoal, freshImageMessage, dynamicContextMsg, usageBaseline, getRunUsage, recordCumulativeUsage, publishGoalJudgeContext, } = args;
|
|
2097
|
+
const { modelFacade, toolExecutor, contextManager, session, fullSystemPrompt, toolDefs, sid, options, profileMaxTurns, cwd, claimClientMessageId, releaseClientMessageId, toolCtx, persistedRunGoal, goalHookHandler, normalizedGoal, freshImageMessage, dynamicContextMsg, usageBaseline, getRunUsage, recordCumulativeUsage, publishGoalJudgeContext, } = args;
|
|
2086
2098
|
// Surface compaction events to the UI so the user knows when context was trimmed.
|
|
2087
2099
|
// Buffer the most recent event so TurnLoop can drain it and emit the
|
|
2088
2100
|
// post_compact hook on the next turn (ContextManager itself doesn't
|
|
@@ -2146,6 +2158,9 @@ export class Engine {
|
|
|
2146
2158
|
releaseClientMessageId,
|
|
2147
2159
|
setOriginClientMessageId: (clientMessageId) => {
|
|
2148
2160
|
toolCtx.originClientMessageId = clientMessageId;
|
|
2161
|
+
const profileId = toolCtx.toolVisibility?.behaviorProfile;
|
|
2162
|
+
const profile = profileId ? this.behaviorProfiles.get(profileId) : undefined;
|
|
2163
|
+
profile?.onUserInputChanged?.(toolCtx.runScopedServices ?? {});
|
|
2149
2164
|
},
|
|
2150
2165
|
recordCumulativeUsage,
|
|
2151
2166
|
onAgentUsage: (usage) => options?.onAgentProgress?.({ type: "usage", usage }),
|
|
@@ -2199,7 +2214,7 @@ export class Engine {
|
|
|
2199
2214
|
// getting re-blocked by the stop-hook until it's done, and the 100
|
|
2200
2215
|
// interactive default would silently truncate a long objective. The
|
|
2201
2216
|
// real backstops are the goal token/time budgets + maxStopBlocks.
|
|
2202
|
-
maxTurns:
|
|
2217
|
+
maxTurns: this.resolveRunMaxTurns(normalizedGoal, profileMaxTurns),
|
|
2203
2218
|
// Consecutive stop-block cap: config override > goal.maxStopBlocks >
|
|
2204
2219
|
// GOAL_DEFAULT_MAX_STOP_BLOCKS(25). The old hardcoded 8 was too tight
|
|
2205
2220
|
// for complex goals that legitimately get re-blocked while advancing.
|
|
@@ -2253,6 +2268,10 @@ export class Engine {
|
|
|
2253
2268
|
});
|
|
2254
2269
|
return turnLoop;
|
|
2255
2270
|
}
|
|
2271
|
+
resolveRunMaxTurns(goal, profileMaxTurns) {
|
|
2272
|
+
const resolved = resolveMaxTurns(this.config.maxTurns, goal);
|
|
2273
|
+
return profileMaxTurns === undefined ? resolved : Math.min(resolved, profileMaxTurns);
|
|
2274
|
+
}
|
|
2256
2275
|
buildSummarizeFn(auxSummaryClient, recordCumulativeUsage) {
|
|
2257
2276
|
return this.auxiliaryPipeline.buildSummarizeFn(auxSummaryClient, recordCumulativeUsage);
|
|
2258
2277
|
}
|
|
@@ -2717,7 +2736,7 @@ export class Engine {
|
|
|
2717
2736
|
: resumed
|
|
2718
2737
|
? `目标已恢复:${next.objective}`
|
|
2719
2738
|
: undefined, {
|
|
2720
|
-
maxTurns:
|
|
2739
|
+
maxTurns: this.resolveRunMaxTurns(next, this.activeProfileMaxTurns),
|
|
2721
2740
|
maxStopBlocks: resolveMaxStopBlocks(this.config.maxStopBlocks, next),
|
|
2722
2741
|
});
|
|
2723
2742
|
if (this.activeGoalHook && !this.activeGoalHookAttached) {
|
|
@@ -3134,6 +3153,19 @@ export class Engine {
|
|
|
3134
3153
|
extendGoalRun(opts) {
|
|
3135
3154
|
if (!this.activeTurnLoop)
|
|
3136
3155
|
return null;
|
|
3156
|
+
if (this.activeProfileMaxTurns !== undefined &&
|
|
3157
|
+
typeof opts.addTurns === "number" &&
|
|
3158
|
+
Number.isFinite(opts.addTurns) &&
|
|
3159
|
+
opts.addTurns > 0 &&
|
|
3160
|
+
opts.addTurns <= Number.MAX_SAFE_INTEGER) {
|
|
3161
|
+
// An empty extension is a no-op that returns the live ceilings. Goal
|
|
3162
|
+
// budgets may still grow, but a behavior-profile ceiling cannot.
|
|
3163
|
+
const currentMaxTurns = this.activeTurnLoop.extend({}).maxTurns;
|
|
3164
|
+
return this.activeTurnLoop.extend({
|
|
3165
|
+
...opts,
|
|
3166
|
+
addTurns: Math.min(opts.addTurns, this.activeProfileMaxTurns - currentMaxTurns),
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3137
3169
|
return this.activeTurnLoop.extend(opts);
|
|
3138
3170
|
}
|
|
3139
3171
|
/**
|
|
@@ -4,6 +4,12 @@ const DEFAULT_MAX_SESSIONS = 256;
|
|
|
4
4
|
const DROP_MIN_PREVIOUS_TOKENS = 100;
|
|
5
5
|
const DROP_MAX_CURRENT_TOKENS = 64;
|
|
6
6
|
const DROP_RATIO = 0.1;
|
|
7
|
+
// Partial cache invalidation can leave thousands of tokens cached. Require
|
|
8
|
+
// both a halving and a meaningful absolute loss to avoid reporting small
|
|
9
|
+
// fluctuations. Compare cache reads, not hit rates: appending uncached input
|
|
10
|
+
// can lower the hit rate while the reusable prefix remains fully cached.
|
|
11
|
+
const PARTIAL_DROP_MAX_RETAINED_RATIO = 0.5;
|
|
12
|
+
const PARTIAL_DROP_MIN_LOST_TOKENS = 4096;
|
|
7
13
|
/**
|
|
8
14
|
* Session stickiness audit. Semantic, capability, and security switches must
|
|
9
15
|
* take effect on the next request even when they invalidate a cache prefix.
|
|
@@ -150,7 +156,10 @@ export class PromptCacheDiagnosticRecorder {
|
|
|
150
156
|
if (previous.cacheReadTokens < DROP_MIN_PREVIOUS_TOKENS)
|
|
151
157
|
return { kind: "updated" };
|
|
152
158
|
const dropRatio = previous.cacheReadTokens > 0 ? cacheReadTokens / previous.cacheReadTokens : 1;
|
|
153
|
-
|
|
159
|
+
const nearColdDrop = cacheReadTokens <= DROP_MAX_CURRENT_TOKENS && dropRatio <= DROP_RATIO;
|
|
160
|
+
const partialDrop = dropRatio <= PARTIAL_DROP_MAX_RETAINED_RATIO &&
|
|
161
|
+
previous.cacheReadTokens - cacheReadTokens >= PARTIAL_DROP_MIN_LOST_TOKENS;
|
|
162
|
+
if (!nearColdDrop && !partialDrop) {
|
|
154
163
|
return { kind: "updated" };
|
|
155
164
|
}
|
|
156
165
|
return {
|
package/dist/engine/run-goal.js
CHANGED
|
@@ -10,9 +10,13 @@ import { logger } from "../logging/logger.js";
|
|
|
10
10
|
/** engine.ts:explicit/stored/fallback 归一化 + 持久化 + goal_set 事件。 */
|
|
11
11
|
export function resolveRunGoal(args) {
|
|
12
12
|
const { options, session, sessionManager, configGoal, isSubAgent, sid, onStream } = args;
|
|
13
|
-
const
|
|
13
|
+
const goalDisabled = options?.disableGoal === true;
|
|
14
|
+
const explicitGoal = goalDisabled ? undefined : normalizeGoal(options?.goal);
|
|
14
15
|
const storedLifecycle = session.state.goalLifecycle;
|
|
15
|
-
const storedGoal =
|
|
16
|
+
const storedGoal = !goalDisabled &&
|
|
17
|
+
isSubAgent !== true &&
|
|
18
|
+
storedLifecycle &&
|
|
19
|
+
isGoalLifecycleCurrent(storedLifecycle)
|
|
16
20
|
? goalConfigFromLifecycle(storedLifecycle)
|
|
17
21
|
: undefined;
|
|
18
22
|
if (storedGoal &&
|
|
@@ -48,7 +52,7 @@ export function resolveRunGoal(args) {
|
|
|
48
52
|
replaced,
|
|
49
53
|
});
|
|
50
54
|
}
|
|
51
|
-
const fallbackGoal = normalizeGoal(configGoal);
|
|
55
|
+
const fallbackGoal = goalDisabled ? undefined : normalizeGoal(configGoal);
|
|
52
56
|
if (fallbackGoal && !fallbackGoal.goalId)
|
|
53
57
|
fallbackGoal.goalId = randomUUID();
|
|
54
58
|
if (fallbackGoal && !fallbackGoal.revision)
|
|
@@ -26,6 +26,13 @@ export interface RunBehaviorProfile {
|
|
|
26
26
|
systemPromptAppend?: string;
|
|
27
27
|
/** Hard tool allowlist for the run (model visibility + execution gate). */
|
|
28
28
|
allowedToolNames?: ReadonlySet<string>;
|
|
29
|
+
/**
|
|
30
|
+
* Positive safe-integer ceiling on model/tool turns. Clamps the existing
|
|
31
|
+
* resolved config/Goal limit, including during live Goal edits/extensions.
|
|
32
|
+
* Invalid values are ignored. The existing no-tools final summary may run
|
|
33
|
+
* once after this ceiling is reached.
|
|
34
|
+
*/
|
|
35
|
+
maxTurns?: number;
|
|
29
36
|
/** When set, the run's permission mode is locked to this value. */
|
|
30
37
|
forcePermissionMode?: NonNullable<EngineConfig["permissionMode"]>;
|
|
31
38
|
/** When true, per-run planMode requests are ignored. */
|
|
@@ -70,6 +77,13 @@ export interface RunBehaviorProfile {
|
|
|
70
77
|
profileParams: Readonly<Record<string, unknown>>;
|
|
71
78
|
reportResult: (key: string, value: unknown) => void;
|
|
72
79
|
}) => Record<string, unknown>;
|
|
80
|
+
/**
|
|
81
|
+
* Called with this run's services after each new steered user message is
|
|
82
|
+
* persisted, before its injection event and the next model step. Profiles
|
|
83
|
+
* may invalidate deferred drafts that answered an earlier input revision.
|
|
84
|
+
* Initial input, queued/revoked steers and duplicate messages do not call it.
|
|
85
|
+
*/
|
|
86
|
+
onUserInputChanged?: (services: Record<string, unknown>) => void;
|
|
73
87
|
/**
|
|
74
88
|
* Per-run metadata exposed to builtin availability guards / definition
|
|
75
89
|
* rewriters via ToolVisibilityContext.profileMeta.
|
|
@@ -98,6 +112,8 @@ export interface EngineRunOptions {
|
|
|
98
112
|
planMode?: boolean;
|
|
99
113
|
approvalRouter?: ApprovalRouter;
|
|
100
114
|
goal?: string | GoalConfig;
|
|
115
|
+
/** Run this standalone turn without explicit, persisted, or configured Goal mode. */
|
|
116
|
+
disableGoal?: boolean;
|
|
101
117
|
injected?: boolean;
|
|
102
118
|
clientMessageId?: string;
|
|
103
119
|
/**
|
|
@@ -54,30 +54,12 @@ export async function resolveRunWorkspace(args) {
|
|
|
54
54
|
? await args.sessionManager.resolveSessionWorkspaceForResume(options.sessionId)
|
|
55
55
|
: undefined;
|
|
56
56
|
if (workspaceResume && !workspaceResume.ok) {
|
|
57
|
-
return
|
|
58
|
-
ok: false,
|
|
59
|
-
result: {
|
|
60
|
-
text: `ERROR: ${workspaceResume.message}`,
|
|
61
|
-
reason: "completed",
|
|
62
|
-
sessionId: options.sessionId,
|
|
63
|
-
turnCount: 0,
|
|
64
|
-
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
65
|
-
},
|
|
66
|
-
};
|
|
57
|
+
return workspaceError(options?.sessionId, workspaceResume.message);
|
|
67
58
|
}
|
|
68
59
|
if (workspaceResume?.ok &&
|
|
69
60
|
workspaceResume.reason === "worktree_missing_branch_gone" &&
|
|
70
61
|
workspaceResume.message) {
|
|
71
|
-
return
|
|
72
|
-
ok: false,
|
|
73
|
-
result: {
|
|
74
|
-
text: workspaceResume.message,
|
|
75
|
-
reason: "completed",
|
|
76
|
-
sessionId: options.sessionId,
|
|
77
|
-
turnCount: 0,
|
|
78
|
-
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
79
|
-
},
|
|
80
|
-
};
|
|
62
|
+
return workspaceError(options?.sessionId, workspaceResume.message);
|
|
81
63
|
}
|
|
82
64
|
// Existing P1 sessions resolve cwd from SessionWorkspace, even if the host
|
|
83
65
|
// passes a stale cwd. Legacy sessions without workspace keep the historical
|
|
@@ -158,7 +140,9 @@ function workspaceError(sessionId, message) {
|
|
|
158
140
|
ok: false,
|
|
159
141
|
result: {
|
|
160
142
|
text: `ERROR: ${message}`,
|
|
161
|
-
|
|
143
|
+
// Like other initialization failures, this run never reached a model
|
|
144
|
+
// turn. Hosts must see a failure rather than report a completed delivery.
|
|
145
|
+
reason: "model_error",
|
|
162
146
|
sessionId: sessionId ?? "workspace-invalid",
|
|
163
147
|
turnCount: 0,
|
|
164
148
|
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
@@ -289,10 +289,10 @@ export declare class TurnLoop {
|
|
|
289
289
|
private stripVolatileContextMessages;
|
|
290
290
|
/**
|
|
291
291
|
* Keep volatile context out of compaction/summarization without moving it on
|
|
292
|
-
* every model round.
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
292
|
+
* every model round. Normalizing a newly appended tool result must not move
|
|
293
|
+
* an earlier volatile snapshot and invalidate an otherwise unchanged prefix.
|
|
294
|
+
* Keep its position when the preceding stable messages remain intact; reset
|
|
295
|
+
* to the tail when that boundary was rewritten or compaction changed length.
|
|
296
296
|
*/
|
|
297
297
|
private restoreVolatileAfterContextManagement;
|
|
298
298
|
private manageContextMessages;
|
package/dist/engine/turn-loop.js
CHANGED
|
@@ -342,16 +342,36 @@ export class TurnLoop {
|
|
|
342
342
|
}
|
|
343
343
|
/**
|
|
344
344
|
* Keep volatile context out of compaction/summarization without moving it on
|
|
345
|
-
* every model round.
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
345
|
+
* every model round. Normalizing a newly appended tool result must not move
|
|
346
|
+
* an earlier volatile snapshot and invalidate an otherwise unchanged prefix.
|
|
347
|
+
* Keep its position when the preceding stable messages remain intact; reset
|
|
348
|
+
* to the tail when that boundary was rewritten or compaction changed length.
|
|
349
349
|
*/
|
|
350
350
|
restoreVolatileAfterContextManagement(original, stableInput, managedStable) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
351
|
+
if (stableInput.length === managedStable.length) {
|
|
352
|
+
// Some cleanup passes copy messages even when their final content is
|
|
353
|
+
// unchanged. Object identity is only a fast path, not a cache boundary.
|
|
354
|
+
const firstChangedIndex = stableInput.findIndex((message, index) => managedStable[index] !== message &&
|
|
355
|
+
JSON.stringify(managedStable[index]) !== JSON.stringify(message));
|
|
356
|
+
if (firstChangedIndex === -1)
|
|
357
|
+
return original;
|
|
358
|
+
let stableIndex = 0;
|
|
359
|
+
let lastVolatileBoundary = 0;
|
|
360
|
+
for (const message of original) {
|
|
361
|
+
if (this.volatileContextMessages.has(message)) {
|
|
362
|
+
lastVolatileBoundary = stableIndex;
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
stableIndex++;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (firstChangedIndex >= lastVolatileBoundary) {
|
|
369
|
+
// Apply the managed tail in place, including its persisted/truncated
|
|
370
|
+
// results. Never restore old payloads just to preserve cache hits.
|
|
371
|
+
stableIndex = 0;
|
|
372
|
+
return original.map((message) => this.volatileContextMessages.has(message) ? message : managedStable[stableIndex++]);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
355
375
|
const volatile = original.filter((message) => this.volatileContextMessages.has(message));
|
|
356
376
|
return [...managedStable, ...volatile];
|
|
357
377
|
}
|
|
@@ -1754,11 +1774,11 @@ export class TurnLoop {
|
|
|
1754
1774
|
break;
|
|
1755
1775
|
}
|
|
1756
1776
|
consumed = true;
|
|
1757
|
-
this.deps.setOriginClientMessageId?.(clientMessageId);
|
|
1758
1777
|
const message = { role: "user", content };
|
|
1759
1778
|
messages.push(message);
|
|
1760
1779
|
this.trackFreshImageMessage(message);
|
|
1761
1780
|
this.deps.transcript.appendMessage("user", content, { steerId: id, clientMessageId });
|
|
1781
|
+
this.deps.setOriginClientMessageId?.(clientMessageId);
|
|
1762
1782
|
this.config.onStream?.({ type: "steer_injected", text, id });
|
|
1763
1783
|
}
|
|
1764
1784
|
return consumed;
|