@corenel/harness 0.13.0 → 0.15.0
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/approvals/service.d.ts.map +1 -1
- package/dist/approvals/service.js +4 -1
- package/dist/approvals/service.js.map +1 -1
- package/dist/audit/log.d.ts.map +1 -1
- package/dist/audit/log.js +12 -1
- package/dist/audit/log.js.map +1 -1
- package/dist/audit/recorder.d.ts +12 -1
- package/dist/audit/recorder.d.ts.map +1 -1
- package/dist/audit/recorder.js +36 -5
- package/dist/audit/recorder.js.map +1 -1
- package/dist/audit/runAudit.d.ts +2 -1
- package/dist/audit/runAudit.d.ts.map +1 -1
- package/dist/audit/runAudit.js +4 -0
- package/dist/audit/runAudit.js.map +1 -1
- package/dist/conversation.d.ts +33 -0
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +44 -1
- package/dist/conversation.js.map +1 -1
- package/dist/core/compactHistory.d.ts +27 -18
- package/dist/core/compactHistory.d.ts.map +1 -1
- package/dist/core/compactHistory.js +106 -40
- package/dist/core/compactHistory.js.map +1 -1
- package/dist/core/dontsRecent.d.ts +5 -0
- package/dist/core/dontsRecent.d.ts.map +1 -0
- package/dist/core/dontsRecent.js +23 -0
- package/dist/core/dontsRecent.js.map +1 -0
- package/dist/core/foldBlocks.d.ts +43 -0
- package/dist/core/foldBlocks.d.ts.map +1 -0
- package/dist/core/foldBlocks.js +114 -0
- package/dist/core/foldBlocks.js.map +1 -0
- package/dist/core/foldTypes.d.ts +25 -0
- package/dist/core/foldTypes.d.ts.map +1 -0
- package/dist/core/foldTypes.js +2 -0
- package/dist/core/foldTypes.js.map +1 -0
- package/dist/core/loop.d.ts +26 -5
- package/dist/core/loop.d.ts.map +1 -1
- package/dist/core/loop.js +274 -74
- package/dist/core/loop.js.map +1 -1
- package/dist/core/recovery.d.ts +1 -0
- package/dist/core/recovery.d.ts.map +1 -1
- package/dist/core/recovery.js +12 -0
- package/dist/core/recovery.js.map +1 -1
- package/dist/core/spawn.d.ts +13 -1
- package/dist/core/spawn.d.ts.map +1 -1
- package/dist/core/spawn.js +10 -1
- package/dist/core/spawn.js.map +1 -1
- package/dist/core/summarize.d.ts +9 -3
- package/dist/core/summarize.d.ts.map +1 -1
- package/dist/core/summarize.js +97 -42
- package/dist/core/summarize.js.map +1 -1
- package/dist/dag/executeNode.d.ts +3 -1
- package/dist/dag/executeNode.d.ts.map +1 -1
- package/dist/dag/executeNode.js +1 -0
- package/dist/dag/executeNode.js.map +1 -1
- package/dist/guardrail/permission-service.d.ts.map +1 -1
- package/dist/guardrail/permission-service.js +31 -11
- package/dist/guardrail/permission-service.js.map +1 -1
- package/dist/guardrail/serialize.d.ts.map +1 -1
- package/dist/guardrail/serialize.js +5 -0
- package/dist/guardrail/serialize.js.map +1 -1
- package/dist/guardrail/validate.d.ts.map +1 -1
- package/dist/guardrail/validate.js +21 -6
- package/dist/guardrail/validate.js.map +1 -1
- package/dist/memory/store.d.ts +3 -0
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/memory/store.js +16 -1
- package/dist/memory/store.js.map +1 -1
- package/dist/orchestrator/record.d.ts +7 -0
- package/dist/orchestrator/record.d.ts.map +1 -1
- package/dist/orchestrator/record.js +14 -2
- package/dist/orchestrator/record.js.map +1 -1
- package/dist/prompts/library/defaults.generated.d.ts +2 -0
- package/dist/prompts/library/defaults.generated.d.ts.map +1 -1
- package/dist/prompts/library/defaults.generated.js +6 -0
- package/dist/prompts/library/defaults.generated.js.map +1 -1
- package/dist/prompts/library/index.d.ts +3 -0
- package/dist/prompts/library/index.d.ts.map +1 -1
- package/dist/prompts/library/index.js +3 -0
- package/dist/prompts/library/index.js.map +1 -1
- package/dist/sessions/directivesStore.d.ts +9 -0
- package/dist/sessions/directivesStore.d.ts.map +1 -0
- package/dist/sessions/directivesStore.js +54 -0
- package/dist/sessions/directivesStore.js.map +1 -0
- package/dist/sessions/fileSessionStore.d.ts +3 -2
- package/dist/sessions/fileSessionStore.d.ts.map +1 -1
- package/dist/sessions/fileSessionStore.js +30 -9
- package/dist/sessions/fileSessionStore.js.map +1 -1
- package/dist/sessions/foldStorage.d.ts +15 -0
- package/dist/sessions/foldStorage.d.ts.map +1 -0
- package/dist/sessions/foldStorage.js +121 -0
- package/dist/sessions/foldStorage.js.map +1 -0
- package/dist/sessions/jsonl.d.ts +6 -0
- package/dist/sessions/jsonl.d.ts.map +1 -1
- package/dist/sessions/jsonl.js +15 -1
- package/dist/sessions/jsonl.js.map +1 -1
- package/dist/sidecar/accessTestRig.d.ts +32 -0
- package/dist/sidecar/accessTestRig.d.ts.map +1 -0
- package/dist/sidecar/accessTestRig.js +107 -0
- package/dist/sidecar/accessTestRig.js.map +1 -0
- package/dist/sidecar/askView.d.ts +20 -0
- package/dist/sidecar/askView.d.ts.map +1 -0
- package/dist/sidecar/askView.js +38 -0
- package/dist/sidecar/askView.js.map +1 -0
- package/dist/sidecar/host.d.ts +139 -11
- package/dist/sidecar/host.d.ts.map +1 -1
- package/dist/sidecar/host.js +394 -13
- package/dist/sidecar/host.js.map +1 -1
- package/dist/sidecar/hostCapability.d.ts +6 -0
- package/dist/sidecar/hostCapability.d.ts.map +1 -0
- package/dist/sidecar/hostCapability.js +7 -0
- package/dist/sidecar/hostCapability.js.map +1 -0
- package/dist/sidecar/identity.d.ts +5 -0
- package/dist/sidecar/identity.d.ts.map +1 -1
- package/dist/sidecar/memberGate.d.ts +5 -0
- package/dist/sidecar/memberGate.d.ts.map +1 -0
- package/dist/sidecar/memberGate.js +128 -0
- package/dist/sidecar/memberGate.js.map +1 -0
- package/dist/sidecar/server.d.ts +425 -11
- package/dist/sidecar/server.d.ts.map +1 -1
- package/dist/sidecar/server.js +1820 -53
- package/dist/sidecar/server.js.map +1 -1
- package/dist/skills.d.ts +7 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +18 -5
- package/dist/skills.js.map +1 -1
- package/dist/tools/builtins.d.ts +17 -0
- package/dist/tools/builtins.d.ts.map +1 -1
- package/dist/tools/builtins.js +22 -2
- package/dist/tools/builtins.js.map +1 -1
- package/dist/tools/callAgent.d.ts +14 -1
- package/dist/tools/callAgent.d.ts.map +1 -1
- package/dist/tools/callAgent.js +18 -2
- package/dist/tools/callAgent.js.map +1 -1
- package/dist/tools/createCrewAgent.d.ts +39 -0
- package/dist/tools/createCrewAgent.d.ts.map +1 -0
- package/dist/tools/createCrewAgent.js +121 -0
- package/dist/tools/createCrewAgent.js.map +1 -0
- package/dist/tools/foldTools.d.ts +5 -0
- package/dist/tools/foldTools.d.ts.map +1 -0
- package/dist/tools/foldTools.js +123 -0
- package/dist/tools/foldTools.js.map +1 -0
- package/dist/tools/skillTool.d.ts.map +1 -1
- package/dist/tools/skillTool.js +7 -2
- package/dist/tools/skillTool.js.map +1 -1
- package/package.json +2 -2
package/dist/core/loop.d.ts
CHANGED
|
@@ -3,9 +3,20 @@ import type { ChatCompletionMessageParam } from 'openai/resources/chat/completio
|
|
|
3
3
|
import type { AgentTool, ToolContext, AgentEvent, Usage } from '@corenel/protocol';
|
|
4
4
|
import { type RecoveryConfig } from './recovery';
|
|
5
5
|
import type { Policy } from '@corenel/protocol';
|
|
6
|
+
import type { DontsJudge } from '@corenel/protocol';
|
|
6
7
|
import type { PermissionService } from '../guardrail/permission-service';
|
|
7
8
|
import { type CompressionConfig } from '../compression';
|
|
9
|
+
import { type FoldSummary } from './foldBlocks';
|
|
10
|
+
import type { FoldStorage } from './foldTypes';
|
|
8
11
|
import { type SkillEntry } from '../tools/skillTool';
|
|
12
|
+
/** What the run ends with when even a folded history will not fit the window. */
|
|
13
|
+
export declare const OVERFLOW_GIVE_UP = "The conversation no longer fits this model's context window even after folding. Start a new chat or switch to a larger-context model.";
|
|
14
|
+
/** The overflow ending when there was nothing a fold could remove (compaction
|
|
15
|
+
* not configured, or nothing collapsible). */
|
|
16
|
+
export declare const OVERFLOW_NO_FOLD = "The conversation no longer fits this model's context window and there is nothing left to fold. Start a new chat or switch to a larger-context model.";
|
|
17
|
+
/** The overflow ending when a fold was possible but did not complete (the
|
|
18
|
+
* summarizer threw, for one). The loop appends the fold error and the 400. */
|
|
19
|
+
export declare const OVERFLOW_FOLD_FAILED = "The conversation no longer fits this model's context window and folding it failed. Try again, or start a new chat.";
|
|
9
20
|
export interface RunAgentArgs {
|
|
10
21
|
client: OpenAI;
|
|
11
22
|
model: string;
|
|
@@ -38,6 +49,12 @@ export interface RunAgentArgs {
|
|
|
38
49
|
/** The spawned child this run is, stamped onto every permission decision it
|
|
39
50
|
* reports (`DecisionEvent.nodeId`). Absent for a root run. */
|
|
40
51
|
decisionNodeId?: string;
|
|
52
|
+
/** Team don'ts (spec 2026-10-07 Part F). Present only on a team daemon run
|
|
53
|
+
* of an agent with don'ts. While present, reply text is held (no
|
|
54
|
+
* `assistant-delta`) and checked before it is committed, and every mutating
|
|
55
|
+
* or owner-level tool call is checked after the permission gate allows it.
|
|
56
|
+
* Absent: the loop is exactly as before. */
|
|
57
|
+
donts?: DontsJudge;
|
|
41
58
|
/** Customize the tool-result text a refused tool call gets. The kernel is
|
|
42
59
|
* host-agnostic and cannot know WHY a decision came back "deny" — a real
|
|
43
60
|
* person declining an interactive prompt, and a non-interactive host that
|
|
@@ -54,13 +71,17 @@ export interface RunAgentArgs {
|
|
|
54
71
|
* then behaves exactly as it did before this existed, which is to say it
|
|
55
72
|
* runs until the provider rejects an oversized request with a fatal 400.
|
|
56
73
|
* `summarize` is a host concern because it costs a model call; the host also
|
|
57
|
-
* decides
|
|
58
|
-
* not an invisible blob). */
|
|
74
|
+
* decides, through `storage`, where a fold is persisted (files the user can
|
|
75
|
+
* edit, not an invisible blob). */
|
|
59
76
|
compaction?: {
|
|
60
|
-
summarize: (collapsed: ChatCompletionMessageParam[]) => Promise<
|
|
61
|
-
|
|
62
|
-
|
|
77
|
+
summarize: (collapsed: ChatCompletionMessageParam[]) => Promise<FoldSummary>;
|
|
78
|
+
/** Where folds are recorded and what backs recall_history/update_directives.
|
|
79
|
+
* Absent: the run still folds, it just records nothing. */
|
|
80
|
+
storage?: FoldStorage;
|
|
63
81
|
};
|
|
82
|
+
/** Fold before the first request regardless of fill: set when the chat's own
|
|
83
|
+
* model is gone and the fallback's window cannot hold the history. */
|
|
84
|
+
forceFoldFirstTurn?: boolean;
|
|
64
85
|
/** Intra-run self-healing budget (stream-retry + tool-steer). Omit for defaults. */
|
|
65
86
|
recovery?: Partial<RecoveryConfig>;
|
|
66
87
|
/** Optional output-token ceiling forwarded to the model on every turn (reasoning
|
package/dist/core/loop.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../core/loop.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,KAAK,EAAE,0BAA0B,EAAsB,MAAM,mCAAmC,CAAC;AACxG,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,KAAK,EAA4B,MAAM,mBAAmB,CAAC;AAE7G,OAAO,EACY,KAAK,cAAc,EAErC,MAAM,YAAY,CAAC;AAMpB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAA0D,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../core/loop.ts"],"names":[],"mappings":"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"}
|
package/dist/core/loop.js
CHANGED
|
@@ -5,8 +5,12 @@ import { initialState, evaluateTurnStart, evaluateToolCall, evaluateCompletion }
|
|
|
5
5
|
import { checkFeature } from '../guards.js';
|
|
6
6
|
import { STANDARD_POLICY } from '../guardrail/builtins.js';
|
|
7
7
|
import { toolAddress } from '../tools/namespaces.js';
|
|
8
|
+
import { DONTS_MAX_STRIKES, DONTS_STOPPED, DONTS_STOP_REASON, DONT_REPLY_TOO_LONG_NOTE, DONT_TOOL_BLOCKED_RESULT, blockedReplyNote, blockedToolNote, } from '@corenel/protocol';
|
|
9
|
+
import { recentForJudge } from './dontsRecent.js';
|
|
8
10
|
import { effectiveThreshold, formatOffloadRef, isOffloadPointer } from '../compression.js';
|
|
9
|
-
import {
|
|
11
|
+
import { FALLBACK_CONTEXT_WINDOW } from './foldBlocks.js';
|
|
12
|
+
import { afterCompaction, compactHistory, compactionDecision, estimateFill, isCompactionMarker } from './compactHistory.js';
|
|
13
|
+
import { makeRecallHistoryTool, makeUpdateDirectivesTool } from '../tools/foldTools.js';
|
|
10
14
|
import { contextNote, contextWindowLine, initialPressureGate, pressureDecision } from './contextPressure.js';
|
|
11
15
|
import { fillToolsMarker } from '../prompts/toolsMarker.js';
|
|
12
16
|
import { createToolLoader, serverIndex } from '../tools/toolSearch.js';
|
|
@@ -14,6 +18,14 @@ import { createSkillTool, skillIndex } from '../tools/skillTool.js';
|
|
|
14
18
|
import { crewToolGuidance } from '../tools/crewList.js';
|
|
15
19
|
import { createTurnVerifier } from './turnVerify.js';
|
|
16
20
|
import { existedBefore, reportFileChanges, FILE_CHANGE_TOOLS } from './fileChanges.js';
|
|
21
|
+
/** What the run ends with when even a folded history will not fit the window. */
|
|
22
|
+
export const OVERFLOW_GIVE_UP = "The conversation no longer fits this model's context window even after folding. Start a new chat or switch to a larger-context model.";
|
|
23
|
+
/** The overflow ending when there was nothing a fold could remove (compaction
|
|
24
|
+
* not configured, or nothing collapsible). */
|
|
25
|
+
export const OVERFLOW_NO_FOLD = "The conversation no longer fits this model's context window and there is nothing left to fold. Start a new chat or switch to a larger-context model.";
|
|
26
|
+
/** The overflow ending when a fold was possible but did not complete (the
|
|
27
|
+
* summarizer threw, for one). The loop appends the fold error and the 400. */
|
|
28
|
+
export const OVERFLOW_FOLD_FAILED = "The conversation no longer fits this model's context window and folding it failed. Try again, or start a new chat.";
|
|
17
29
|
/** The most tools one request may carry.
|
|
18
30
|
*
|
|
19
31
|
* OpenAI-compatible providers refuse the whole request above this -- "Invalid
|
|
@@ -37,6 +49,18 @@ export async function runAgent(args) {
|
|
|
37
49
|
const verifier = createTurnVerifier({ kv: toolCtx.kv, files: toolCtx.files });
|
|
38
50
|
const policy = args.policy ?? STANDARD_POLICY;
|
|
39
51
|
const permission = args.permission;
|
|
52
|
+
const donts = args.donts;
|
|
53
|
+
/** The resolved permission level of a call the don'ts judge checks: a
|
|
54
|
+
* mutating call, or one whose level is `owner`. Null for every other call,
|
|
55
|
+
* for an unknown tool, and always without the port. */
|
|
56
|
+
const dontsCheckedLevel = (tool) => {
|
|
57
|
+
if (!donts || !tool)
|
|
58
|
+
return null;
|
|
59
|
+
const level = permission
|
|
60
|
+
? permission.resolveLevel({ name: tool.name, address: toolAddress(tool), mutates: tool.mutates, permission: tool.permission, roomSafe: tool.roomSafe })
|
|
61
|
+
: 'allow';
|
|
62
|
+
return tool.mutates === true || level === 'owner' ? level : null;
|
|
63
|
+
};
|
|
40
64
|
const compression = args.compression;
|
|
41
65
|
const ctxWindow = compression?.contextWindow;
|
|
42
66
|
const compressionStrategy = compression?.strategy ?? 'eager';
|
|
@@ -101,13 +125,17 @@ export async function runAgent(args) {
|
|
|
101
125
|
* appears is an exact count of what this run added -- maintained by nothing,
|
|
102
126
|
* and therefore impossible for a later `push` site to get wrong. */
|
|
103
127
|
const incoming = new Set(args.messages);
|
|
128
|
+
/* A blocked-reply note is a system message this run appended. It is counted,
|
|
129
|
+
* never mistaken for the system prompt the walk below stops at. Empty
|
|
130
|
+
* without the don'ts port. */
|
|
131
|
+
const dontsNotes = new Set();
|
|
104
132
|
const appendedFromTail = () => {
|
|
105
133
|
let n = 0;
|
|
106
134
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
107
135
|
const m = messages[i];
|
|
108
|
-
// A
|
|
136
|
+
// A fold's own summary/directives blocks, and the system prompt unshifted
|
|
109
137
|
// below, are fresh objects too -- and neither is something this run said.
|
|
110
|
-
if (incoming.has(m) || m.role === 'system' || isCompactionMarker(m))
|
|
138
|
+
if (incoming.has(m) || (m.role === 'system' && !dontsNotes.has(m)) || isCompactionMarker(m))
|
|
111
139
|
break;
|
|
112
140
|
n++;
|
|
113
141
|
}
|
|
@@ -122,7 +150,14 @@ export async function runAgent(args) {
|
|
|
122
150
|
? createToolLoader({ deferred, loaded: args.loadedTools, onChange: (names) => emit({ type: 'tools-loaded', names }) })
|
|
123
151
|
: null;
|
|
124
152
|
const skills = args.skills ?? [];
|
|
125
|
-
const
|
|
153
|
+
const storage = args.compaction?.storage;
|
|
154
|
+
const foldTools = [
|
|
155
|
+
...(storage?.transcript ? [makeRecallHistoryTool(storage.transcript)] : []),
|
|
156
|
+
...(storage?.directives ? [makeUpdateDirectivesTool(storage.directives)] : []),
|
|
157
|
+
];
|
|
158
|
+
/* Fold tools LAST: when MAX_PROVIDER_TOOLS caps this list, they must never be
|
|
159
|
+
* what pushes `use_skill` or `tool_search` off the end. */
|
|
160
|
+
const eager = [...tools, ...(skills.length ? [createSkillTool(skills)] : []), ...(loader ? [loader.searchTool] : []), ...foldTools];
|
|
126
161
|
const capped = (list) => (list.length > MAX_PROVIDER_TOOLS ? list.slice(0, MAX_PROVIDER_TOOLS) : list);
|
|
127
162
|
const activeTools = () => capped(loader ? [...eager, ...loader.loaded()] : eager);
|
|
128
163
|
let sent = activeTools();
|
|
@@ -197,6 +232,82 @@ export async function runAgent(args) {
|
|
|
197
232
|
const known = new Map([...eager, ...deferred].map((t) => [t.name, t]));
|
|
198
233
|
await compactIncomingOffloads(messages, known, compression, toolCtx.recall, emit);
|
|
199
234
|
}
|
|
235
|
+
const foldWindow = ctxWindow || FALLBACK_CONTEXT_WINDOW;
|
|
236
|
+
let forceFold = !!args.forceFoldFirstTurn;
|
|
237
|
+
/* One fold, used by three triggers: the fill gate, a forced fold (model
|
|
238
|
+
* switch), and the overflow retry. Rebuilds `messages` in place -- callers
|
|
239
|
+
* (and `res.messages`) hold this array. Returns what happened; on 'failed'
|
|
240
|
+
* the reason is left in `foldError` for the overflow message.
|
|
241
|
+
*
|
|
242
|
+
* A summarizer failure is the same shape as a persist failure: the window is
|
|
243
|
+
* full, the model call meant to compress it did not come back, and the honest
|
|
244
|
+
* response is to carry on UNFOLDED rather than end the run on the recovery
|
|
245
|
+
* path. The turn proceeds with the history it had, which is exactly what
|
|
246
|
+
* would have happened without compaction at all. */
|
|
247
|
+
let foldError = '';
|
|
248
|
+
const fold = async (offloadFill) => {
|
|
249
|
+
if (!args.compaction)
|
|
250
|
+
return 'nothing';
|
|
251
|
+
try {
|
|
252
|
+
let override = null;
|
|
253
|
+
try {
|
|
254
|
+
override = (await storage?.directives?.read()) ?? null;
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
alog('directives read failed', e instanceof Error ? e.message : String(e));
|
|
258
|
+
}
|
|
259
|
+
const out = await compactHistory(messages, {
|
|
260
|
+
summarize: args.compaction.summarize,
|
|
261
|
+
contextWindow: foldWindow,
|
|
262
|
+
directivesOverride: override,
|
|
263
|
+
recallAvailable: !!storage?.transcript,
|
|
264
|
+
offloadOverBytes: compression && compression.mode !== 'off'
|
|
265
|
+
? effectiveThreshold(compression.mode, offloadFill)
|
|
266
|
+
: undefined,
|
|
267
|
+
offload: toolCtx.recall
|
|
268
|
+
// Recorded under 'user' rather than a tool name: the RecallStore is
|
|
269
|
+
// indifferent to who authored the bulk, but the transcript should not
|
|
270
|
+
// claim a tool produced what a person pasted.
|
|
271
|
+
? async (content) => formatOffloadRef(await toolCtx.recall.record('user', content))
|
|
272
|
+
: undefined,
|
|
273
|
+
});
|
|
274
|
+
/* Nothing to collapse: no fold happened, so nothing is recorded or emitted. */
|
|
275
|
+
if (!out)
|
|
276
|
+
return 'nothing';
|
|
277
|
+
messages.splice(0, messages.length, ...out.messages);
|
|
278
|
+
/* The 'deferred' strategy holds ABSOLUTE indices into `messages`, to
|
|
279
|
+
* rewrite those entries once the model has seen them in full. The splice
|
|
280
|
+
* above just invalidated every one of them, and the rewrite pass read
|
|
281
|
+
* `.role` off the resulting undefined -- a crash in the interactive
|
|
282
|
+
* CLI's shipping configuration ('deferred' + compaction). Nothing is
|
|
283
|
+
* lost by dropping them: the tool results they point at were collapsed
|
|
284
|
+
* into the summary and are not in `messages` any more. */
|
|
285
|
+
deferredOffloads.length = 0;
|
|
286
|
+
/* The fold has ALREADY happened by this point. Failing to RECORD it must
|
|
287
|
+
* not take the run with it -- the checkpoint store deliberately rejects
|
|
288
|
+
* rather than risk clobbering an existing CHECKPOINT.md, and a read-only
|
|
289
|
+
* workspace rejects too. Dying here would lose the whole conversation in
|
|
290
|
+
* order to save nothing. */
|
|
291
|
+
try {
|
|
292
|
+
await storage?.onFold?.({ summary: out.summary, directives: out.directives, collapsed: out.collapsed });
|
|
293
|
+
}
|
|
294
|
+
catch (e) {
|
|
295
|
+
alog('fold persist failed', e instanceof Error ? e.message : String(e));
|
|
296
|
+
}
|
|
297
|
+
emit({ type: 'checkpoint', collapsed: out.collapsedCount, checkpoint: out.summary });
|
|
298
|
+
compactGate = afterCompaction();
|
|
299
|
+
return 'folded';
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
/* Re-arm so a later turn may try again rather than giving up for good.
|
|
303
|
+
* The floor is left as it was: it describes where the last SUCCESSFUL
|
|
304
|
+
* fold landed, and a failed attempt moved nothing. */
|
|
305
|
+
compactGate = { ...compactGate, armed: true };
|
|
306
|
+
foldError = e instanceof Error ? e.message : String(e);
|
|
307
|
+
alog('compaction failed', foldError);
|
|
308
|
+
return 'failed';
|
|
309
|
+
}
|
|
310
|
+
};
|
|
200
311
|
const usage = { promptTokens: 0, completionTokens: 0 };
|
|
201
312
|
/** Model calls made, and the largest single request among them. */
|
|
202
313
|
let calls = 0;
|
|
@@ -205,6 +316,9 @@ export async function runAgent(args) {
|
|
|
205
316
|
let stopReason = 'stop';
|
|
206
317
|
let lastToolSig = null;
|
|
207
318
|
let stopNudges = 0;
|
|
319
|
+
/** Denials by the don'ts judge in this run; the third stops it. Counted per
|
|
320
|
+
* loop: a callee's own port counts its own (preflight F20). */
|
|
321
|
+
let strikes = 0;
|
|
208
322
|
let lastStreamErr = ''; // the blip a stream retry recovered from (for the heal record)
|
|
209
323
|
for (let turn = 1; unlimitedTurns || turn <= maxTurns; turn++) {
|
|
210
324
|
if (signal.aborted) {
|
|
@@ -221,74 +335,26 @@ export async function runAgent(args) {
|
|
|
221
335
|
emit({ type: 'injected', content: typeof m.content === 'string' ? m.content : '' });
|
|
222
336
|
}
|
|
223
337
|
}
|
|
224
|
-
/* Rung-2
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* the fatal 400. Fall back to reading the window off the messages. Only as
|
|
234
|
-
* a fallback: a real reading is always better than an estimate that cannot
|
|
235
|
-
* see the tool schemas. */
|
|
338
|
+
/* Rung-2 fold, BEFORE the request is built so the folded array is what goes
|
|
339
|
+
* on the wire. `fill` is last turn's prompt/window ratio; the gate refuses
|
|
340
|
+
* below the threshold and refuses again when there is nothing collapsible,
|
|
341
|
+
* because a fold that removes nothing still rewrites the cached prefix and
|
|
342
|
+
* re-bills everything after it. On turn 1 there is no usage reading yet, so
|
|
343
|
+
* `fill` is 0 and the gate could not fire before the first request -- which
|
|
344
|
+
* is how a resumed, already-oversized conversation took the fatal 400. Fall
|
|
345
|
+
* back to reading the window off the messages; only as a fallback: a real
|
|
346
|
+
* reading beats an estimate that cannot see the tool schemas. */
|
|
236
347
|
const gateFill = fill || estimateFill(messages, ctxWindow);
|
|
237
|
-
const
|
|
348
|
+
const prevGate = compactGate;
|
|
349
|
+
const gate = compactionDecision(gateFill, messages, compactGate, foldWindow);
|
|
238
350
|
compactGate = gate.next;
|
|
239
|
-
if (args.compaction && gate.compact) {
|
|
240
|
-
|
|
241
|
-
/*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
try {
|
|
247
|
-
const out = await compactHistory(messages, {
|
|
248
|
-
summarize: args.compaction.summarize,
|
|
249
|
-
anchor: args.compaction.anchor,
|
|
250
|
-
offloadOverBytes: compression && compression.mode !== 'off'
|
|
251
|
-
? effectiveThreshold(compression.mode, gateFill)
|
|
252
|
-
: undefined,
|
|
253
|
-
offload: toolCtx.recall
|
|
254
|
-
// Recorded under 'user' rather than a tool name: the RecallStore is
|
|
255
|
-
// indifferent to who authored the bulk, but the transcript should not
|
|
256
|
-
// claim a tool produced what a person pasted.
|
|
257
|
-
? async (content) => formatOffloadRef(await toolCtx.recall.record('user', content))
|
|
258
|
-
: undefined,
|
|
259
|
-
});
|
|
260
|
-
messages.splice(0, messages.length, ...out.messages);
|
|
261
|
-
/* The 'deferred' strategy holds ABSOLUTE indices into `messages`, to
|
|
262
|
-
* rewrite those entries once the model has seen them in full. The splice
|
|
263
|
-
* above just invalidated every one of them, and the rewrite pass read
|
|
264
|
-
* `.role` off the resulting undefined -- a crash in the interactive
|
|
265
|
-
* CLI's shipping configuration ('deferred' + compaction). Nothing is
|
|
266
|
-
* lost by dropping them: the tool results they point at were collapsed
|
|
267
|
-
* into the checkpoint and are not in `messages` any more. */
|
|
268
|
-
deferredOffloads.length = 0;
|
|
269
|
-
/* The compaction has ALREADY happened by this point. Failing to RECORD it
|
|
270
|
-
* must not take the run with it -- checkpointStore.append deliberately
|
|
271
|
-
* rejects rather than risk clobbering an existing CHECKPOINT.md, and a
|
|
272
|
-
* read-only workspace rejects too. Dying here would lose the whole
|
|
273
|
-
* conversation in order to save nothing. */
|
|
274
|
-
try {
|
|
275
|
-
await args.compaction.onCheckpoint?.(out.checkpoint);
|
|
276
|
-
}
|
|
277
|
-
catch (e) {
|
|
278
|
-
alog('checkpoint persist failed', e instanceof Error ? e.message : String(e));
|
|
279
|
-
}
|
|
280
|
-
emit({ type: 'checkpoint', collapsed: before - messages.length, checkpoint: out.checkpoint });
|
|
281
|
-
}
|
|
282
|
-
catch (e) {
|
|
283
|
-
/* Compaction failed (most likely the summarizer's own model call). The
|
|
284
|
-
* turn proceeds with the history it had -- exactly what would have
|
|
285
|
-
* happened had compaction never been configured. Re-arm so a later turn
|
|
286
|
-
* may try again rather than giving up on the run for good. */
|
|
287
|
-
/* The floor is left as it was: it describes where the last SUCCESSFUL
|
|
288
|
-
* compaction landed, and a failed attempt moved nothing. */
|
|
289
|
-
compactGate = { ...compactGate, armed: true };
|
|
290
|
-
alog('compaction failed', e instanceof Error ? e.message : String(e));
|
|
291
|
-
}
|
|
351
|
+
if (args.compaction && (gate.compact || forceFold)) {
|
|
352
|
+
forceFold = false;
|
|
353
|
+
/* The gate fired but there was nothing to fold (the only bulk is a
|
|
354
|
+
* pending tool call, held back): nothing was spent, so the latch is not
|
|
355
|
+
* either -- it fires again once there is something to collapse. */
|
|
356
|
+
if ((await fold(gateFill)) === 'nothing' && gate.compact)
|
|
357
|
+
compactGate = prevGate;
|
|
292
358
|
}
|
|
293
359
|
/* CONTEXT PRESSURE. After the compaction block, deliberately: if a
|
|
294
360
|
* compaction just ran, the window it drained is the one the agent should
|
|
@@ -357,6 +423,8 @@ export async function runAgent(args) {
|
|
|
357
423
|
turnAc = new AbortController();
|
|
358
424
|
if (signal.aborted)
|
|
359
425
|
turnAc.abort();
|
|
426
|
+
let overflowRetried = false;
|
|
427
|
+
let foldRetryOpen = false; // a 'retrying' fold-retry heal record awaits its outcome
|
|
360
428
|
for (let sAttempt = 0;;) {
|
|
361
429
|
try {
|
|
362
430
|
const result = await streamCompletion(client, {
|
|
@@ -366,9 +434,16 @@ export async function runAgent(args) {
|
|
|
366
434
|
tool_choice: toolDefs.length ? 'auto' : undefined,
|
|
367
435
|
max_completion_tokens: args.maxCompletionTokens,
|
|
368
436
|
temperature: args.temperature,
|
|
369
|
-
}, turnAc.signal,
|
|
437
|
+
}, turnAc.signal,
|
|
438
|
+
// A held reply is never streamed: it is shown only once it passes.
|
|
439
|
+
(delta) => { if (!donts)
|
|
440
|
+
emit({ type: 'assistant-delta', delta }); });
|
|
370
441
|
if (sAttempt > 0)
|
|
371
442
|
emit({ type: 'heal', record: { site: 'stream', kind: 'transient', strategy: 'retry-backoff', attempt: sAttempt, outcome: 'recovered', error: lastStreamErr } });
|
|
443
|
+
if (foldRetryOpen) {
|
|
444
|
+
foldRetryOpen = false;
|
|
445
|
+
emit({ type: 'heal', record: { site: 'stream', kind: 'overflow', strategy: 'fold-retry', attempt: 1, outcome: 'recovered', error: lastStreamErr } });
|
|
446
|
+
}
|
|
372
447
|
streamed = result;
|
|
373
448
|
break;
|
|
374
449
|
}
|
|
@@ -388,6 +463,26 @@ export async function runAgent(args) {
|
|
|
388
463
|
const msg = String(e?.message || e);
|
|
389
464
|
lastStreamErr = msg;
|
|
390
465
|
const kind = classifyStreamError(msg);
|
|
466
|
+
/* A context-length rejection is not transient: re-sending the same
|
|
467
|
+
* history fails the same way. Fold once and re-send (`fold` rewrote
|
|
468
|
+
* `messages` in place and streamCompletion reads it by reference); a
|
|
469
|
+
* second overflow means folding could not make it fit. */
|
|
470
|
+
if (kind === 'overflow') {
|
|
471
|
+
const outcome = overflowRetried ? null : await fold(1);
|
|
472
|
+
if (outcome === 'folded') {
|
|
473
|
+
overflowRetried = true;
|
|
474
|
+
foldRetryOpen = true;
|
|
475
|
+
emit({ type: 'heal', record: { site: 'stream', kind, strategy: 'fold-retry', attempt: sAttempt + 1, outcome: 'retrying', error: msg } });
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
emit({ type: 'heal', record: { site: 'stream', kind, strategy: 'give-up', attempt: sAttempt + 1, outcome: 'abandoned', error: msg } });
|
|
479
|
+
const ending = overflowRetried ? `${OVERFLOW_GIVE_UP} (${msg})`
|
|
480
|
+
: outcome === 'failed' ? `${OVERFLOW_FOLD_FAILED} (${foldError}; ${msg})`
|
|
481
|
+
: `${OVERFLOW_NO_FOLD} (${msg})`;
|
|
482
|
+
emit({ type: 'error', message: ending });
|
|
483
|
+
streamFail = 'error';
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
391
486
|
sAttempt++;
|
|
392
487
|
if (shouldRetryStream(kind, sAttempt, recovery)) {
|
|
393
488
|
emit({ type: 'heal', record: { site: 'stream', kind, strategy: 'retry-backoff', attempt: sAttempt, outcome: 'retrying', error: msg } });
|
|
@@ -460,6 +555,46 @@ export async function runAgent(args) {
|
|
|
460
555
|
compactGate = { armed: false, floor: fill };
|
|
461
556
|
}
|
|
462
557
|
emit({ type: 'usage', usage: streamed.usage, model });
|
|
558
|
+
/* TEAM DON'TS: the reply is checked before anything of this turn is
|
|
559
|
+
* committed, its tool calls included, so a denied reply runs none of them
|
|
560
|
+
* and leaves no tool_call without a result. A denied reply is never
|
|
561
|
+
* emitted or kept: the model gets a note instead and answers again. */
|
|
562
|
+
if (donts && streamed.content) {
|
|
563
|
+
emit({ type: 'donts-checking', subject: 'reply' });
|
|
564
|
+
const held = await donts.checkReply({ text: streamed.content, turn, recent: recentForJudge(messages) });
|
|
565
|
+
if (held.kind === 'cancelled') {
|
|
566
|
+
stopReason = 'aborted';
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
if (held.kind === 'denied') {
|
|
570
|
+
strikes++;
|
|
571
|
+
if (strikes >= DONTS_MAX_STRIKES) {
|
|
572
|
+
emit({ type: 'error', message: DONTS_STOPPED });
|
|
573
|
+
stopReason = DONTS_STOP_REASON;
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
const note = { role: 'system', content: held.tooLong ? DONT_REPLY_TOO_LONG_NOTE : blockedReplyNote(held.rule, held.by ?? 'nobody') };
|
|
577
|
+
dontsNotes.add(note);
|
|
578
|
+
messages.push(note);
|
|
579
|
+
if (!unlimitedTurns && turn === maxTurns)
|
|
580
|
+
stopReason = 'max_turns';
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
/* TEAM DON'TS (preflight F4 amended, F5): every call the judge will check,
|
|
585
|
+
* by id, with its resolved level. Such a call is committed with '{}' as its
|
|
586
|
+
* arguments and gets its real ones back only once it is announced (it
|
|
587
|
+
* passed, or a person allowed it), so a held excerpt is never stored on any
|
|
588
|
+
* exit path. The raw arguments stay in `streamed.toolCalls`, which is what
|
|
589
|
+
* runs and what the no-progress signature compares. Empty without the port. */
|
|
590
|
+
const dontsChecked = new Map();
|
|
591
|
+
if (donts) {
|
|
592
|
+
for (const t of streamed.toolCalls) {
|
|
593
|
+
const level = dontsCheckedLevel(toolByName.get(t.name));
|
|
594
|
+
if (level !== null)
|
|
595
|
+
dontsChecked.set(t.id, level);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
463
598
|
// Commit the assistant message to history (with tool_calls if any).
|
|
464
599
|
const assistantMsg = streamed.toolCalls.length
|
|
465
600
|
? {
|
|
@@ -468,7 +603,7 @@ export async function runAgent(args) {
|
|
|
468
603
|
tool_calls: streamed.toolCalls.map((t) => ({
|
|
469
604
|
id: t.id,
|
|
470
605
|
type: 'function',
|
|
471
|
-
function: { name: t.name, arguments: t.arguments },
|
|
606
|
+
function: { name: t.name, arguments: dontsChecked.has(t.id) ? '{}' : t.arguments },
|
|
472
607
|
})),
|
|
473
608
|
}
|
|
474
609
|
: { role: 'assistant', content: streamed.content };
|
|
@@ -503,18 +638,31 @@ export async function runAgent(args) {
|
|
|
503
638
|
}
|
|
504
639
|
lastToolSig = sig;
|
|
505
640
|
let policyStop = false;
|
|
641
|
+
let dontsStop = false;
|
|
642
|
+
let dontsCancelled = false;
|
|
506
643
|
for (const call of streamed.toolCalls) {
|
|
507
644
|
let parsed = {};
|
|
508
645
|
try {
|
|
509
646
|
parsed = call.arguments ? JSON.parse(call.arguments) : {};
|
|
510
647
|
}
|
|
511
648
|
catch {
|
|
512
|
-
|
|
649
|
+
// A checked call's arguments are held (preflight F4): never echoed back.
|
|
650
|
+
const invalid = dontsChecked.has(call.id)
|
|
651
|
+
? DONT_INVALID_JSON_RESULT
|
|
652
|
+
: `Error: arguments were not valid JSON: ${call.arguments}`;
|
|
653
|
+
messages.push({ role: 'tool', tool_call_id: call.id, content: invalid });
|
|
513
654
|
emit({ type: 'tool-result', id: call.id, name: call.name, result: 'invalid JSON arguments', isError: true });
|
|
514
655
|
continue;
|
|
515
656
|
}
|
|
516
657
|
alog('tool', call.name, parsed);
|
|
517
|
-
|
|
658
|
+
/* TEAM DON'TS (preflight F5): a call the judge will check is announced
|
|
659
|
+
* only once it passes or a person allows it, so its arguments never
|
|
660
|
+
* reach the asker before then. `checkedLevel` is null for every call
|
|
661
|
+
* that is not checked, and always without the port: those emit here,
|
|
662
|
+
* exactly as before. */
|
|
663
|
+
const checkedLevel = dontsChecked.get(call.id) ?? null;
|
|
664
|
+
if (checkedLevel === null)
|
|
665
|
+
emit({ type: 'tool-call', id: call.id, name: call.name, args: parsed });
|
|
518
666
|
const tool = toolByName.get(call.name);
|
|
519
667
|
if (!tool) {
|
|
520
668
|
messages.push({ role: 'tool', tool_call_id: call.id, content: `Error: unknown tool "${call.name}"` });
|
|
@@ -595,6 +743,36 @@ export async function runAgent(args) {
|
|
|
595
743
|
continue;
|
|
596
744
|
}
|
|
597
745
|
}
|
|
746
|
+
/* TEAM DON'TS: after the permission gate allowed it, never before, so a
|
|
747
|
+
* call refused by policy or by a person never reaches the judge. Only a
|
|
748
|
+
* mutating call or one whose level is `owner` (`checkedLevel` above). */
|
|
749
|
+
if (donts && checkedLevel !== null) {
|
|
750
|
+
const mutating = tool.mutates === true;
|
|
751
|
+
emit({ type: 'donts-checking', subject: 'tool', tool: tool.name });
|
|
752
|
+
const held = await donts.checkTool({ tool: tool.name, input: parsed, level: checkedLevel, mutating, recent: recentForJudge(messages) });
|
|
753
|
+
if (held.kind === 'cancelled') {
|
|
754
|
+
dontsCancelled = true;
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
if (held.kind === 'denied') {
|
|
758
|
+
strikes++;
|
|
759
|
+
// The note carries the rule, never the arguments (preflight F4).
|
|
760
|
+
messages.push({ role: 'tool', tool_call_id: call.id, content: blockedToolNote(tool.name, held.rule, held.by ?? 'nobody') });
|
|
761
|
+
// Announced with empty arguments (final fix M1): a live surface pairs
|
|
762
|
+
// the blocked result with a call, while the arguments stay held.
|
|
763
|
+
emit({ type: 'tool-call', id: call.id, name: call.name, args: {} });
|
|
764
|
+
emit({ type: 'tool-result', id: call.id, name: tool.name, result: DONT_TOOL_BLOCKED_RESULT, isError: true });
|
|
765
|
+
if (tool.mutates)
|
|
766
|
+
verifier.denied(call.id, tool.name);
|
|
767
|
+
if (strikes >= DONTS_MAX_STRIKES) {
|
|
768
|
+
dontsStop = true;
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
setCallArguments(assistantMsg, call.id, call.arguments);
|
|
774
|
+
emit({ type: 'tool-call', id: call.id, name: call.name, args: parsed });
|
|
775
|
+
}
|
|
598
776
|
if (tool.mutates)
|
|
599
777
|
await verifier.before(call.id, tool.name, parsed);
|
|
600
778
|
// Only when someone is logging file changes, and only for a tool that
|
|
@@ -681,6 +859,15 @@ export async function runAgent(args) {
|
|
|
681
859
|
emit({ type: 'tool-result', id: call.id, name: call.name, result: errMsg, isError: true, durationMs: Date.now() - startedAt });
|
|
682
860
|
}
|
|
683
861
|
}
|
|
862
|
+
if (dontsCancelled) {
|
|
863
|
+
stopReason = 'aborted';
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
if (dontsStop) {
|
|
867
|
+
emit({ type: 'error', message: DONTS_STOPPED });
|
|
868
|
+
stopReason = DONTS_STOP_REASON;
|
|
869
|
+
break;
|
|
870
|
+
}
|
|
684
871
|
if (policyStop) {
|
|
685
872
|
stopReason = 'policy';
|
|
686
873
|
break;
|
|
@@ -705,6 +892,19 @@ export async function runAgent(args) {
|
|
|
705
892
|
emit({ type: 'done', reason: stopReason });
|
|
706
893
|
return { messages, text, usage, stopReason, appended: appendedFromTail(), contextTokens, calls };
|
|
707
894
|
}
|
|
895
|
+
/** Team don'ts: a checked call was committed with '{}' as its arguments; once
|
|
896
|
+
* it is announced its real arguments are put back in the stored message. */
|
|
897
|
+
function setCallArguments(msg, callId, args) {
|
|
898
|
+
if (msg.role !== 'assistant' || !msg.tool_calls)
|
|
899
|
+
return;
|
|
900
|
+
for (const t of msg.tool_calls) {
|
|
901
|
+
if (t.id === callId)
|
|
902
|
+
t.function.arguments = args;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
/** The model-facing result for a checked call whose arguments were not valid
|
|
906
|
+
* JSON: fixed text, so the held arguments are not echoed into the transcript. */
|
|
907
|
+
const DONT_INVALID_JSON_RESULT = 'Error: the tool arguments were not valid JSON.';
|
|
708
908
|
/** 'deferred' strategy: compact any ALREADY-oversized tool result already
|
|
709
909
|
* sitting in the incoming history — i.e. produced by an earlier `runAgent()`
|
|
710
910
|
* call (a prior conversation turn, a resumed session) rather than this one.
|