@nexrall/code-core 1.4.66 → 1.4.67
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/agent/agentTypes.d.ts +6 -2
- package/dist/agent/agentTypes.d.ts.map +1 -1
- package/dist/agent/agentTypes.js +13 -4
- package/dist/agent/askOnce.d.ts +12 -0
- package/dist/agent/askOnce.d.ts.map +1 -0
- package/dist/agent/askOnce.js +41 -0
- package/dist/agent/compaction.d.ts +244 -0
- package/dist/agent/compaction.d.ts.map +1 -0
- package/dist/agent/compaction.js +976 -0
- package/dist/agent/fileLocks.d.ts +34 -0
- package/dist/agent/fileLocks.d.ts.map +1 -0
- package/dist/agent/fileLocks.js +114 -0
- package/dist/agent/hooks.d.ts +324 -0
- package/dist/agent/hooks.d.ts.map +1 -0
- package/dist/agent/hooks.js +1228 -0
- package/dist/agent/iterationPolicy.d.ts +121 -0
- package/dist/agent/iterationPolicy.d.ts.map +1 -0
- package/dist/agent/iterationPolicy.js +297 -0
- package/dist/agent/lifecycleHost.d.ts +55 -0
- package/dist/agent/lifecycleHost.d.ts.map +1 -0
- package/dist/agent/lifecycleHost.js +294 -0
- package/dist/agent/loop.d.ts +11 -491
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +417 -3031
- package/dist/agent/planMode.d.ts.map +1 -1
- package/dist/agent/planMode.js +1 -0
- package/dist/agent/sharedTasks.d.ts +7 -0
- package/dist/agent/sharedTasks.d.ts.map +1 -1
- package/dist/agent/sharedTasks.js +16 -0
- package/dist/agent/subAgentBudget.d.ts +65 -0
- package/dist/agent/subAgentBudget.d.ts.map +1 -0
- package/dist/agent/subAgentBudget.js +269 -0
- package/dist/agent/subTask.d.ts +6 -0
- package/dist/agent/subTask.d.ts.map +1 -0
- package/dist/agent/subTask.js +713 -0
- package/dist/agent/subTaskSupport.d.ts +156 -0
- package/dist/agent/subTaskSupport.d.ts.map +1 -0
- package/dist/agent/subTaskSupport.js +409 -0
- package/dist/agent/toolDescriptions.d.ts +3 -0
- package/dist/agent/toolDescriptions.d.ts.map +1 -0
- package/dist/agent/toolDescriptions.js +116 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +17 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/mcp/client.d.ts +104 -0
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +136 -2
- package/dist/mcp/httpClient.d.ts +17 -1
- package/dist/mcp/httpClient.d.ts.map +1 -1
- package/dist/mcp/httpClient.js +120 -19
- package/dist/mcp/manager.d.ts +77 -2
- package/dist/mcp/manager.d.ts.map +1 -1
- package/dist/mcp/manager.js +275 -9
- package/dist/mcp/sseClient.d.ts +7 -1
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +45 -1
- package/dist/mcp/stats.d.ts +41 -0
- package/dist/mcp/stats.d.ts.map +1 -0
- package/dist/mcp/stats.js +108 -0
- package/dist/permissions/destructive.d.ts +2 -0
- package/dist/permissions/destructive.d.ts.map +1 -1
- package/dist/permissions/destructive.js +6 -2
- package/dist/permissions/destructiveTokens.d.ts +5 -0
- package/dist/permissions/destructiveTokens.d.ts.map +1 -1
- package/dist/permissions/destructiveTokens.js +9 -3
- package/dist/permissions/modePolicy.d.ts.map +1 -1
- package/dist/permissions/modePolicy.js +5 -2
- package/dist/permissions/rules.d.ts +4 -1
- package/dist/permissions/rules.d.ts.map +1 -1
- package/dist/permissions/rules.js +29 -0
- package/dist/types.d.ts +45 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { ToolResult } from '../types';
|
|
2
|
+
import { type AgentMemoryScope } from './agentTypes';
|
|
3
|
+
export declare const MAX_ITERATIONS_CEILING = 2000;
|
|
4
|
+
export declare const STALL_LIMIT = 8;
|
|
5
|
+
export declare const REPEAT_STALL_LIMIT = 12;
|
|
6
|
+
export declare const EMPTY_TURN_RETRY_LIMIT = 3;
|
|
7
|
+
/** Backoff for the Nth (0-based) empty-turn retry. Pure, so the schedule is testable. */
|
|
8
|
+
export declare function emptyTurnBackoffMs(attempt: number): number;
|
|
9
|
+
/**
|
|
10
|
+
* Should an empty assistant turn be retried automatically, rather than surfaced?
|
|
11
|
+
*
|
|
12
|
+
* Split out as a pure function because the two "empty" causes look IDENTICAL at the
|
|
13
|
+
* call site (both are `content.length === 0`) and telling them apart is the entire
|
|
14
|
+
* point — the previous bug in this area was treating a deterministic truncation as a
|
|
15
|
+
* transient hiccup and advising a retry that reproduced it verbatim.
|
|
16
|
+
*
|
|
17
|
+
* • `max_tokens` → the model burned its whole output budget on reasoning and was CUT
|
|
18
|
+
* OFF. Retrying re-runs the same prompt with the same budget and the same reasoning
|
|
19
|
+
* behaviour, so it fails the same way while billing again. Never retried; the user
|
|
20
|
+
* is told to lower effort or split the task (stopReasonNotice 'output-limit').
|
|
21
|
+
* • anything else → a genuinely transient empty turn. Retried, up to the limit.
|
|
22
|
+
*
|
|
23
|
+
* The attempt cap matters as much as the classification: a model that has decided to
|
|
24
|
+
* return nothing (e.g. a prompt it refuses to continue) would otherwise loop forever
|
|
25
|
+
* on a paid endpoint. After the cap we fall back to the old visible notice.
|
|
26
|
+
*/
|
|
27
|
+
export declare function shouldRetryEmptyTurn(stopReason: string | null | undefined, attemptsSoFar: number): boolean;
|
|
28
|
+
/** Empty-turn retry limits, exposed for tests. */
|
|
29
|
+
export declare const _emptyTurnRetry: {
|
|
30
|
+
EMPTY_TURN_RETRY_LIMIT: number;
|
|
31
|
+
EMPTY_TURN_RETRY_BASE_MS: number;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Fingerprint one round's tool failures, for the repeated-failure runaway guard.
|
|
35
|
+
*
|
|
36
|
+
* Exported (with the limits) purely as a test seam: the guard's whole value is in the
|
|
37
|
+
* edge cases — that a DIFFERENT error each round must NOT trip it, that call order
|
|
38
|
+
* within a round is irrelevant, that a long error body doesn't make every occurrence
|
|
39
|
+
* look unique — and none of that is reachable without driving a live model loop.
|
|
40
|
+
*
|
|
41
|
+
* Sorted so parallel tool calls completing in a different order still compare equal;
|
|
42
|
+
* truncated because errors often embed a varying path or timestamp late in the string.
|
|
43
|
+
*/
|
|
44
|
+
export declare function errorRoundSignature(errored: Array<{
|
|
45
|
+
name: string;
|
|
46
|
+
error: string;
|
|
47
|
+
}>): string;
|
|
48
|
+
/** Runaway-guard limits, exposed for tests. */
|
|
49
|
+
export declare const _stallLimits: {
|
|
50
|
+
STALL_LIMIT: number;
|
|
51
|
+
REPEAT_STALL_LIMIT: number;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* The single tool granted by a `memory:` frontmatter scope.
|
|
55
|
+
*
|
|
56
|
+
* Named distinctly from `memory_write` on purpose: they write to DIFFERENT stores, and
|
|
57
|
+
* a model that saw one name for both would reasonably assume its notes were visible to
|
|
58
|
+
* the main agent. They are not.
|
|
59
|
+
*/
|
|
60
|
+
export declare const AGENT_MEMORY_TOOL = "agent_memory_write";
|
|
61
|
+
/**
|
|
62
|
+
* Schema advertised to the model, only for a sub-agent with a declared memory scope.
|
|
63
|
+
*
|
|
64
|
+
* The description does the load-bearing work of keeping the two stores apart in the
|
|
65
|
+
* model's head: it must not believe these notes reach the user or the main agent.
|
|
66
|
+
*/
|
|
67
|
+
export declare const AGENT_MEMORY_TOOL_SCHEMA: {
|
|
68
|
+
readonly name: "agent_memory_write";
|
|
69
|
+
readonly description: string;
|
|
70
|
+
readonly input_schema: {
|
|
71
|
+
readonly type: "object";
|
|
72
|
+
readonly properties: {
|
|
73
|
+
readonly content: {
|
|
74
|
+
readonly type: "string";
|
|
75
|
+
readonly description: "The single fact to remember, 1-2 sentences.";
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
readonly required: readonly ["content"];
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
/** Identifies whose notes an agent_memory_write call may touch. */
|
|
82
|
+
export interface AgentMemoryBinding {
|
|
83
|
+
agentName: string;
|
|
84
|
+
scope: AgentMemoryScope;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Execute an `agent_memory_write` call.
|
|
88
|
+
*
|
|
89
|
+
* Pure-ish and exported so the refusal paths are testable without spawning a real
|
|
90
|
+
* sub-agent: the binding is data, so "no binding" and "unsafe agent name" can both be
|
|
91
|
+
* exercised directly.
|
|
92
|
+
*/
|
|
93
|
+
export declare function executeAgentMemoryWrite(input: Record<string, unknown>, binding: AgentMemoryBinding | undefined, workDir?: string): Promise<ToolResult>;
|
|
94
|
+
/**
|
|
95
|
+
* Why an agent run stopped.
|
|
96
|
+
*
|
|
97
|
+
* `'clean'` and `'aborted'` are the two silent-by-design outcomes: the model gave a
|
|
98
|
+
* final answer, or the user pressed Ctrl+C and already knows why it stopped.
|
|
99
|
+
* EVERYTHING else owes the user an explanation, which is what stopReasonNotice covers.
|
|
100
|
+
*/
|
|
101
|
+
export type StopReason = 'clean' | 'aborted' | 'reported-elsewhere' | 'empty-response' | 'output-limit' | 'no-balance' | 'no-team-budget' | 'team-unavailable' | 'stalled' | 'stalled-repeat' | 'budget' | 'hook-blocked' | 'unknown';
|
|
102
|
+
/**
|
|
103
|
+
* The message shown when a run ends for any reason other than a clean finish.
|
|
104
|
+
*
|
|
105
|
+
* Pure and exported so every branch is testable: reaching some of these for real needs
|
|
106
|
+
* an empty wallet, a dead upstream, or hundreds of iterations. Returns null only for
|
|
107
|
+
* the two outcomes that are deliberately silent.
|
|
108
|
+
*
|
|
109
|
+
* `'unknown'` deliberately produces a message rather than nothing. If a future `break`
|
|
110
|
+
* forgets to set a reason, the symptom should be a visible "ended unexpectedly" line —
|
|
111
|
+
* annoying and reportable — not the silent stop that made this refactor necessary.
|
|
112
|
+
*/
|
|
113
|
+
export declare function stopReasonNotice(reason: StopReason, ctx?: {
|
|
114
|
+
budget?: number;
|
|
115
|
+
repeatError?: string | null;
|
|
116
|
+
hookReason?: string | null;
|
|
117
|
+
}): string | null;
|
|
118
|
+
export declare const TEAM_SCOPE_ERROR_CODES: ReadonlySet<string>;
|
|
119
|
+
export declare function resolveMaxIterations(optionValue: number | undefined, settingsRaw: Record<string, unknown>): number;
|
|
120
|
+
export declare function resolveAutoContinue(optionValue: boolean | undefined, settingsRaw: Record<string, unknown>): boolean;
|
|
121
|
+
//# sourceMappingURL=iterationPolicy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"iterationPolicy.d.ts","sourceRoot":"","sources":["../../src/agent/iterationPolicy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAYrD,eAAO,MAAM,sBAAsB,OAAQ,CAAC;AAU5C,eAAO,MAAM,WAAW,IAAgB,CAAC;AAKzC,eAAO,MAAM,kBAAkB,KAAU,CAAC;AAuB1C,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAKxC,yFAAyF;AACzF,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACrC,aAAa,EAAE,MAAM,GACpB,OAAO,CAGT;AAED,kDAAkD;AAClD,eAAO,MAAM,eAAe;;;CAAuD,CAAC;AAEpF;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,GAC9C,MAAM,CAKR;AAED,+CAA+C;AAC/C,eAAO,MAAM,YAAY;;;CAAsC,CAAC;AAEhE;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,uBAAuB,CAAC;AAEtD;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;CAgB3B,CAAC;AAEX,mEAAmE;AACnE,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,kBAAkB,GAAG,SAAS,EACvC,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,CAAC,CA8BrB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAClB,OAAO,GACP,SAAS,GACT,oBAAoB,GACpB,gBAAgB,GAChB,cAAc,GACd,YAAY,GACZ,gBAAgB,GAChB,kBAAkB,GAClB,SAAS,GACT,gBAAgB,GAChB,QAAQ,GACR,cAAc,GACd,SAAS,CAAC;AAEd;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,UAAU,EAClB,GAAG,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GACrF,MAAM,GAAG,IAAI,CA4Df;AAMD,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAMrD,CAAC;AAaH,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAYR;AAKD,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,OAAO,GAAG,SAAS,EAChC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,OAAO,CAQT"}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TEAM_SCOPE_ERROR_CODES = exports.AGENT_MEMORY_TOOL_SCHEMA = exports.AGENT_MEMORY_TOOL = exports._stallLimits = exports._emptyTurnRetry = exports.EMPTY_TURN_RETRY_LIMIT = exports.REPEAT_STALL_LIMIT = exports.STALL_LIMIT = exports.MAX_ITERATIONS_CEILING = void 0;
|
|
4
|
+
exports.emptyTurnBackoffMs = emptyTurnBackoffMs;
|
|
5
|
+
exports.shouldRetryEmptyTurn = shouldRetryEmptyTurn;
|
|
6
|
+
exports.errorRoundSignature = errorRoundSignature;
|
|
7
|
+
exports.executeAgentMemoryWrite = executeAgentMemoryWrite;
|
|
8
|
+
exports.stopReasonNotice = stopReasonNotice;
|
|
9
|
+
exports.resolveMaxIterations = resolveMaxIterations;
|
|
10
|
+
exports.resolveAutoContinue = resolveAutoContinue;
|
|
11
|
+
const memory_1 = require("./memory");
|
|
12
|
+
// ─── Iteration cap ──────────────────────────────────────────────────────────
|
|
13
|
+
// Each iteration is one model response + one round of tool execution. The cap is
|
|
14
|
+
// a runaway-loop backstop, NOT a task-size limit — on a large project a single
|
|
15
|
+
// task can legitimately need well over 50 rounds (read → search → edit → test →
|
|
16
|
+
// fix → …). A too-low cap makes the agent appear to "freeze" mid-task. Keep the
|
|
17
|
+
// default high and let projects raise it further via settings / env.
|
|
18
|
+
const DEFAULT_MAX_ITERATIONS = 500;
|
|
19
|
+
exports.MAX_ITERATIONS_CEILING = 2000; // default auto-continue backstop (no explicit opt-in)
|
|
20
|
+
// There is deliberately NO hard ceiling on an explicit opt-in anymore: a task meant to run
|
|
21
|
+
// for days/weeks/months (an unattended agent loop) must not die at an arbitrary iteration
|
|
22
|
+
// count just because someone picked a big-but-finite safety number in the past. The actual
|
|
23
|
+
// protection against a runaway session burning cost forever is STALL_LIMIT /
|
|
24
|
+
// REPEAT_STALL_LIMIT below — those catch "stuck", not "long", and fire in a handful of
|
|
25
|
+
// rounds regardless of how high maxIterations is set. `Infinity` here is a real, intentional
|
|
26
|
+
// value (not a bug) — resolveMaxIterations() below returns it whenever the caller does not
|
|
27
|
+
// explicitly opt in to a finite number, matching the wording "unbounded unless you cap it".
|
|
28
|
+
const HARD_ITERATIONS_CAP = Infinity; // no absolute cap — bounded only by the stall guards
|
|
29
|
+
exports.STALL_LIMIT = 8; // consecutive all-failed tool rounds → give up (runaway guard)
|
|
30
|
+
// Consecutive rounds producing the IDENTICAL error(s) → give up, even if other calls in
|
|
31
|
+
// those rounds succeeded. Higher than STALL_LIMIT because a repeat is weaker evidence of
|
|
32
|
+
// being stuck than a total failure: legitimately retrying one failing command a few times
|
|
33
|
+
// while making progress elsewhere is normal, twelve times is not.
|
|
34
|
+
exports.REPEAT_STALL_LIMIT = 12;
|
|
35
|
+
// ─── Empty-turn auto-retry ────────────────────────────────────────────────────
|
|
36
|
+
//
|
|
37
|
+
// A model turn can come back STRUCTURALLY FINE (the stream completed, `message_complete`
|
|
38
|
+
// arrived) and still contain nothing usable once thinking blocks are stripped for
|
|
39
|
+
// history. Anthropic models hit this materially more often than the OpenAI-compatible
|
|
40
|
+
// ones, because only they emit `thinking`/`redacted_thinking` blocks — a turn that
|
|
41
|
+
// reasons and then ends without committing text or a tool call strips down to `[]`.
|
|
42
|
+
//
|
|
43
|
+
// The transport layer cannot fix this. client.ts only retries when the stream produced
|
|
44
|
+
// NOTHING; here the stream produced a valid message that happens to be empty, so it
|
|
45
|
+
// resolves normally and the loop used to break immediately and tell the user to type
|
|
46
|
+
// "continue" — asking a human to press a button the loop can press itself.
|
|
47
|
+
//
|
|
48
|
+
// Retrying here is safe for a specific, checkable reason, not a hopeful one:
|
|
49
|
+
// • tools are dispatched from `assistantMessage.content` AFTER this check, and this
|
|
50
|
+
// content is empty, so no tool ran and no file changed;
|
|
51
|
+
// • nothing was pushed to `messages` (the push happens below this branch), so the
|
|
52
|
+
// history is byte-identical to the previous attempt — a retry is a clean re-ask,
|
|
53
|
+
// not a resend of a corrupted turn;
|
|
54
|
+
// • no round was counted, no hook fired, no checkpoint was taken.
|
|
55
|
+
// Contrast the output-limit case, which is NOT retried: see shouldRetryEmptyTurn.
|
|
56
|
+
exports.EMPTY_TURN_RETRY_LIMIT = 3;
|
|
57
|
+
// Short, escalating pause (1s, 2s, 4s). Long enough to ride out the upstream blip that
|
|
58
|
+
// causes this; short enough that three failures cost ~7s rather than a visible stall.
|
|
59
|
+
const EMPTY_TURN_RETRY_BASE_MS = 1000;
|
|
60
|
+
/** Backoff for the Nth (0-based) empty-turn retry. Pure, so the schedule is testable. */
|
|
61
|
+
function emptyTurnBackoffMs(attempt) {
|
|
62
|
+
return EMPTY_TURN_RETRY_BASE_MS * Math.pow(2, Math.max(0, attempt));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Should an empty assistant turn be retried automatically, rather than surfaced?
|
|
66
|
+
*
|
|
67
|
+
* Split out as a pure function because the two "empty" causes look IDENTICAL at the
|
|
68
|
+
* call site (both are `content.length === 0`) and telling them apart is the entire
|
|
69
|
+
* point — the previous bug in this area was treating a deterministic truncation as a
|
|
70
|
+
* transient hiccup and advising a retry that reproduced it verbatim.
|
|
71
|
+
*
|
|
72
|
+
* • `max_tokens` → the model burned its whole output budget on reasoning and was CUT
|
|
73
|
+
* OFF. Retrying re-runs the same prompt with the same budget and the same reasoning
|
|
74
|
+
* behaviour, so it fails the same way while billing again. Never retried; the user
|
|
75
|
+
* is told to lower effort or split the task (stopReasonNotice 'output-limit').
|
|
76
|
+
* • anything else → a genuinely transient empty turn. Retried, up to the limit.
|
|
77
|
+
*
|
|
78
|
+
* The attempt cap matters as much as the classification: a model that has decided to
|
|
79
|
+
* return nothing (e.g. a prompt it refuses to continue) would otherwise loop forever
|
|
80
|
+
* on a paid endpoint. After the cap we fall back to the old visible notice.
|
|
81
|
+
*/
|
|
82
|
+
function shouldRetryEmptyTurn(stopReason, attemptsSoFar) {
|
|
83
|
+
if (stopReason === 'max_tokens')
|
|
84
|
+
return false;
|
|
85
|
+
return attemptsSoFar < exports.EMPTY_TURN_RETRY_LIMIT;
|
|
86
|
+
}
|
|
87
|
+
/** Empty-turn retry limits, exposed for tests. */
|
|
88
|
+
exports._emptyTurnRetry = { EMPTY_TURN_RETRY_LIMIT: exports.EMPTY_TURN_RETRY_LIMIT, EMPTY_TURN_RETRY_BASE_MS };
|
|
89
|
+
/**
|
|
90
|
+
* Fingerprint one round's tool failures, for the repeated-failure runaway guard.
|
|
91
|
+
*
|
|
92
|
+
* Exported (with the limits) purely as a test seam: the guard's whole value is in the
|
|
93
|
+
* edge cases — that a DIFFERENT error each round must NOT trip it, that call order
|
|
94
|
+
* within a round is irrelevant, that a long error body doesn't make every occurrence
|
|
95
|
+
* look unique — and none of that is reachable without driving a live model loop.
|
|
96
|
+
*
|
|
97
|
+
* Sorted so parallel tool calls completing in a different order still compare equal;
|
|
98
|
+
* truncated because errors often embed a varying path or timestamp late in the string.
|
|
99
|
+
*/
|
|
100
|
+
function errorRoundSignature(errored) {
|
|
101
|
+
return errored
|
|
102
|
+
.map(({ name, error }) => `${name}:${String(error).slice(0, 200)}`)
|
|
103
|
+
.sort()
|
|
104
|
+
.join('|');
|
|
105
|
+
}
|
|
106
|
+
/** Runaway-guard limits, exposed for tests. */
|
|
107
|
+
exports._stallLimits = { STALL_LIMIT: exports.STALL_LIMIT, REPEAT_STALL_LIMIT: exports.REPEAT_STALL_LIMIT };
|
|
108
|
+
/**
|
|
109
|
+
* The single tool granted by a `memory:` frontmatter scope.
|
|
110
|
+
*
|
|
111
|
+
* Named distinctly from `memory_write` on purpose: they write to DIFFERENT stores, and
|
|
112
|
+
* a model that saw one name for both would reasonably assume its notes were visible to
|
|
113
|
+
* the main agent. They are not.
|
|
114
|
+
*/
|
|
115
|
+
exports.AGENT_MEMORY_TOOL = 'agent_memory_write';
|
|
116
|
+
/**
|
|
117
|
+
* Schema advertised to the model, only for a sub-agent with a declared memory scope.
|
|
118
|
+
*
|
|
119
|
+
* The description does the load-bearing work of keeping the two stores apart in the
|
|
120
|
+
* model's head: it must not believe these notes reach the user or the main agent.
|
|
121
|
+
*/
|
|
122
|
+
exports.AGENT_MEMORY_TOOL_SCHEMA = {
|
|
123
|
+
name: exports.AGENT_MEMORY_TOOL,
|
|
124
|
+
description: 'Save a durable note to YOUR OWN persistent notes, which are injected into your prompt on ' +
|
|
125
|
+
'future runs. Use this for lessons that will still be true next time — a convention this repo ' +
|
|
126
|
+
'follows, a recurring bug pattern, a command that works, a dead end not worth retrying. ' +
|
|
127
|
+
'These notes are PRIVATE to you: they are NOT shown to the user and NOT read by the main agent, ' +
|
|
128
|
+
'so anything the user or the main agent needs to know must still go in your final message. ' +
|
|
129
|
+
'One self-contained fact per call, a sentence or two. Do not save transient task details.',
|
|
130
|
+
input_schema: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {
|
|
133
|
+
content: { type: 'string', description: 'The single fact to remember, 1-2 sentences.' },
|
|
134
|
+
},
|
|
135
|
+
required: ['content'],
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Execute an `agent_memory_write` call.
|
|
140
|
+
*
|
|
141
|
+
* Pure-ish and exported so the refusal paths are testable without spawning a real
|
|
142
|
+
* sub-agent: the binding is data, so "no binding" and "unsafe agent name" can both be
|
|
143
|
+
* exercised directly.
|
|
144
|
+
*/
|
|
145
|
+
async function executeAgentMemoryWrite(input, binding, workDir) {
|
|
146
|
+
// Belt-and-braces: the permission gate already refuses this tool without a binding.
|
|
147
|
+
// Re-checked because this is the function that actually touches the filesystem, and
|
|
148
|
+
// it must not depend on a caller elsewhere having got the check right.
|
|
149
|
+
if (!binding) {
|
|
150
|
+
return {
|
|
151
|
+
error: `${exports.AGENT_MEMORY_TOOL} is only available to a sub-agent whose definition declares a \`memory:\` ` +
|
|
152
|
+
'scope. Put anything worth remembering in your final message instead.',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const content = typeof input.content === 'string' ? input.content.trim() : '';
|
|
156
|
+
if (!content)
|
|
157
|
+
return { error: `${exports.AGENT_MEMORY_TOOL} requires a non-empty \`content\` string.` };
|
|
158
|
+
const res = await (0, memory_1.writeAgentMemory)(binding.agentName, binding.scope, content, workDir);
|
|
159
|
+
if (!res.ok) {
|
|
160
|
+
// The realistic cause is a repo-scoped store with no workDir, or an agent name that
|
|
161
|
+
// is not a safe single path segment. Say which, rather than a bare failure.
|
|
162
|
+
return {
|
|
163
|
+
error: `Could not save to the "${binding.agentName}" agent's ${binding.scope} notes. ` +
|
|
164
|
+
'Either this scope needs a project directory (use `memory: user` for a store that ' +
|
|
165
|
+
'works anywhere) or the agent name is not usable as a filename.',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
output: res.already
|
|
170
|
+
? 'Already saved (a near-identical note exists) — nothing added.'
|
|
171
|
+
: `Saved to your ${binding.scope} notes. It will be in your prompt on your next run.`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The message shown when a run ends for any reason other than a clean finish.
|
|
176
|
+
*
|
|
177
|
+
* Pure and exported so every branch is testable: reaching some of these for real needs
|
|
178
|
+
* an empty wallet, a dead upstream, or hundreds of iterations. Returns null only for
|
|
179
|
+
* the two outcomes that are deliberately silent.
|
|
180
|
+
*
|
|
181
|
+
* `'unknown'` deliberately produces a message rather than nothing. If a future `break`
|
|
182
|
+
* forgets to set a reason, the symptom should be a visible "ended unexpectedly" line —
|
|
183
|
+
* annoying and reportable — not the silent stop that made this refactor necessary.
|
|
184
|
+
*/
|
|
185
|
+
function stopReasonNotice(reason, ctx = {}) {
|
|
186
|
+
switch (reason) {
|
|
187
|
+
case 'clean':
|
|
188
|
+
case 'aborted':
|
|
189
|
+
// A dedicated channel (e.g. the zero-balance bubble via onBalanceStatus) has already
|
|
190
|
+
// told the user why this stopped. Naming this case explicitly — rather than reusing
|
|
191
|
+
// 'aborted' — keeps "the user cancelled" from silently coming to mean two things.
|
|
192
|
+
case 'reported-elsewhere':
|
|
193
|
+
return null;
|
|
194
|
+
// Reaching this notice now means the loop ALREADY retried automatically and the
|
|
195
|
+
// model came back empty every time (see shouldRetryEmptyTurn). Saying "send
|
|
196
|
+
// continue to retry" without that context reads as if nothing was tried, and the
|
|
197
|
+
// user's manual retry is then the fourth identical attempt — so name the attempts.
|
|
198
|
+
case 'empty-response':
|
|
199
|
+
return `\n\u26a0\ufe0f The model returned an empty response ${exports.EMPTY_TURN_RETRY_LIMIT} times in a row, so nothing was done. ` +
|
|
200
|
+
`This is usually a transient upstream hiccup that the agent retries by itself; it did not clear this time. ` +
|
|
201
|
+
`Send "continue" to try again, or switch model with /model if it persists.\n`;
|
|
202
|
+
// Deliberately NOT folded into 'empty-response'. Both arrive as an assistant
|
|
203
|
+
// message with no content once thinking blocks are stripped, so they used to
|
|
204
|
+
// be indistinguishable — and the user was told the truncation case was "a
|
|
205
|
+
// transient upstream hiccup" they should retry with "continue". Both halves
|
|
206
|
+
// were wrong: nothing was transient, and continuing re-runs the same prompt
|
|
207
|
+
// with the same budget and the same reasoning behaviour, reproducing it
|
|
208
|
+
// exactly. Naming the cause is the fix; the advice has to change with it.
|
|
209
|
+
case 'output-limit':
|
|
210
|
+
return `\n\u26a0\ufe0f The model used its entire output budget on internal reasoning and was cut off ` +
|
|
211
|
+
`before it could reply, so nothing was done. This will repeat identically if you just retry \u2014 ` +
|
|
212
|
+
`lower the reasoning effort (/effort) or split the task into smaller steps.\n`;
|
|
213
|
+
case 'no-balance':
|
|
214
|
+
return `\n\ud83d\udcb3 Stopped: your balance is empty, so the request was rejected before it started. ` +
|
|
215
|
+
`Top up and send "continue" \u2014 no tokens were used for this turn.\n`;
|
|
216
|
+
case 'no-team-budget':
|
|
217
|
+
return `\n\ud83d\udcb3 Stopped: the team budget you are billing to is empty, so the request was rejected ` +
|
|
218
|
+
`before it started. Ask your team admin to add funds \u2014 or switch "Bill to" back to Personal \u2014 ` +
|
|
219
|
+
`and send "continue". No tokens were used for this turn.\n`;
|
|
220
|
+
case 'team-unavailable':
|
|
221
|
+
return `\n\u26a0\ufe0f Stopped: that team is no longer available for billing (you may have been removed, or the ` +
|
|
222
|
+
`team was suspended). "Bill to" has been reset to your Personal account, so sending "continue" will ` +
|
|
223
|
+
`run on your own wallet.\n`;
|
|
224
|
+
case 'stalled':
|
|
225
|
+
return `\n\ud83d\uded1 Stopped: the last ${exports.STALL_LIMIT} tool rounds all failed, so the agent looked stuck. ` +
|
|
226
|
+
`Fix the underlying error (or grant the needed permission) and send "continue".\n`;
|
|
227
|
+
case 'stalled-repeat':
|
|
228
|
+
return `\n\ud83d\uded1 Stopped: the same tool error repeated ${exports.REPEAT_STALL_LIMIT} rounds in a row, so the agent ` +
|
|
229
|
+
`was looping without making progress.` +
|
|
230
|
+
(ctx.repeatError ? ` The recurring error was:\n${ctx.repeatError}\n` : '\n') +
|
|
231
|
+
`Fix that underlying cause (or grant the needed permission) and send "continue".\n`;
|
|
232
|
+
case 'hook-blocked':
|
|
233
|
+
return `\n\u26d4 A PostToolBatch hook stopped the turn before the next model call` +
|
|
234
|
+
(ctx.hookReason ? `: ${ctx.hookReason}` : '.') +
|
|
235
|
+
`\nThe reason is in the conversation, so send "continue" once the cause is handled.\n`;
|
|
236
|
+
case 'budget':
|
|
237
|
+
return `\n\u23f8\ufe0f Stopped at the ${ctx.budget}-step safety limit \u2014 the task may be incomplete. ` +
|
|
238
|
+
`Send "continue" to resume, or raise the limit via "maxIterations" in .nexrall/settings.json ` +
|
|
239
|
+
`(or the NEXRALL_MAX_ITERATIONS env var). Auto-continue can be disabled with "autoContinue": false.\n`;
|
|
240
|
+
case 'unknown':
|
|
241
|
+
default:
|
|
242
|
+
return `\n\u26a0\ufe0f The run ended unexpectedly without completing. Your work so far is preserved \u2014 ` +
|
|
243
|
+
`send "continue" to resume.\n`;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Error codes that mean "this team cannot pay for anything right now" — the
|
|
247
|
+
// pre-flight refusals routes/code.js returns when a request names a team
|
|
248
|
+
// (migration 139). Kept as one list so the loop's auto-fallback and the server's
|
|
249
|
+
// vocabulary cannot drift apart silently.
|
|
250
|
+
exports.TEAM_SCOPE_ERROR_CODES = new Set([
|
|
251
|
+
'team_unavailable',
|
|
252
|
+
'team_not_found',
|
|
253
|
+
'not_a_member',
|
|
254
|
+
'member_suspended',
|
|
255
|
+
'team_suspended',
|
|
256
|
+
]);
|
|
257
|
+
// Resolve the soft iteration budget. Precedence:
|
|
258
|
+
// options.maxIterations → env NEXRALL_MAX_ITERATIONS → settings.maxIterations → default
|
|
259
|
+
//
|
|
260
|
+
// The DEFAULT (nothing set) is clamped to MAX_ITERATIONS_CEILING so an ordinary
|
|
261
|
+
// run can never spin past 2000 rounds by accident. But an EXPLICIT value from
|
|
262
|
+
// any of the three opt-in channels is honoured with NO upper bound (HARD_ITERATIONS_CAP
|
|
263
|
+
// is Infinity) — this is what lets a genuinely unattended, long-lived task (an agent meant
|
|
264
|
+
// to keep working for days or longer) run for as many rounds as it needs once the user has
|
|
265
|
+
// deliberately asked for that, instead of dying at a hidden ceiling while the error message
|
|
266
|
+
// misleadingly tells them to "raise the limit". A stuck/looping run is still caught by the
|
|
267
|
+
// STALL_LIMIT / REPEAT_STALL_LIMIT guards below, independent of this budget.
|
|
268
|
+
function resolveMaxIterations(optionValue, settingsRaw) {
|
|
269
|
+
const fromEnv = Number(process.env.NEXRALL_MAX_ITERATIONS);
|
|
270
|
+
const fromSettings = Number(settingsRaw.maxIterations);
|
|
271
|
+
const explicit = (typeof optionValue === 'number' && optionValue > 0) ? optionValue
|
|
272
|
+
: Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv
|
|
273
|
+
: Number.isFinite(fromSettings) && fromSettings > 0 ? fromSettings
|
|
274
|
+
: undefined;
|
|
275
|
+
if (explicit === undefined)
|
|
276
|
+
return DEFAULT_MAX_ITERATIONS;
|
|
277
|
+
// Explicit opt-in: honour it verbatim (floor for a fractional value from JSON/env), no
|
|
278
|
+
// longer bounded by an absolute safety cap — see HARD_ITERATIONS_CAP's comment above.
|
|
279
|
+
return Math.min(Math.floor(explicit), HARD_ITERATIONS_CAP);
|
|
280
|
+
}
|
|
281
|
+
// When the soft budget is exhausted with work still pending, keep going instead
|
|
282
|
+
// of stopping. Precedence: options.autoContinue → env NEXRALL_AUTO_CONTINUE →
|
|
283
|
+
// settings.autoContinue → default (on). Bounded by STALL_LIMIT and the ceiling.
|
|
284
|
+
function resolveAutoContinue(optionValue, settingsRaw) {
|
|
285
|
+
if (typeof optionValue === 'boolean')
|
|
286
|
+
return optionValue;
|
|
287
|
+
const env = process.env.NEXRALL_AUTO_CONTINUE;
|
|
288
|
+
if (env === '0' || env === 'false')
|
|
289
|
+
return false;
|
|
290
|
+
if (env === '1' || env === 'true')
|
|
291
|
+
return true;
|
|
292
|
+
const s = settingsRaw.autoContinue;
|
|
293
|
+
if (typeof s === 'boolean')
|
|
294
|
+
return s;
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
//# sourceMappingURL=iterationPolicy.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** One instruction file that was loaded, told to the InstructionsLoaded hooks. */
|
|
2
|
+
export interface LoadedInstruction {
|
|
3
|
+
file_path: string;
|
|
4
|
+
/** Claude Code's memory_type vocabulary, mapped onto nex's files (see the CLI caller). */
|
|
5
|
+
memory_type: 'User' | 'Project' | 'Local' | 'Managed';
|
|
6
|
+
/** Claude Code's load_reason vocabulary: session_start | compact | include | nested_traversal | path_glob_match. */
|
|
7
|
+
load_reason: string;
|
|
8
|
+
globs?: string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Tell the InstructionsLoaded hooks which instruction files this session loaded.
|
|
12
|
+
* Observer: output and exit code are ignored (Claude Code says the same for this event).
|
|
13
|
+
*
|
|
14
|
+
* Called by the host at the moment the files are actually read — a `--bare` run reads
|
|
15
|
+
* none and therefore fires nothing, which is the honest behaviour.
|
|
16
|
+
*/
|
|
17
|
+
export declare function fireInstructionsLoadedHooks(workDir: string, sessionId: string, files: LoadedInstruction[]): Promise<void>;
|
|
18
|
+
export interface FileChangedWatcher {
|
|
19
|
+
/** Stop watching and drop any pending debounce timer. Safe to call twice. */
|
|
20
|
+
stop(): void;
|
|
21
|
+
/** Replace the DYNAMIC watch list (a hook's watchPaths answer). Static matcher names stay. */
|
|
22
|
+
updateWatchPaths(paths: string[]): void;
|
|
23
|
+
/** What the watcher is currently listening for — exposed for tests and `nex doctor`. */
|
|
24
|
+
watchedFiles(): string[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Start watching the files named by the FileChanged hooks' matchers ("a.txt|b.txt").
|
|
28
|
+
*
|
|
29
|
+
* - Returns null when no FileChanged hook is configured, so callers can wire it
|
|
30
|
+
* unconditionally.
|
|
31
|
+
* - `fs.watch` on the WORKING DIRECTORY (not the files themselves: editors replace
|
|
32
|
+
* files, which kills a per-file watch on rename) filtered to the watched basenames,
|
|
33
|
+
* with a short per-file debounce — one save is several fs events.
|
|
34
|
+
* - A hook's `{"watchPaths": […]}` answer replaces the dynamic list; `{"systemMessage": "…"}`
|
|
35
|
+
* is handed to `onSystemMessage` for the host to display.
|
|
36
|
+
* - Never throws into the host: watching is best-effort (an OS without inotify, a
|
|
37
|
+
* directory that vanishes) and a broken watch must not break the session.
|
|
38
|
+
*/
|
|
39
|
+
export declare function startFileChangedWatcher(workDir: string, sessionId: string, opts?: {
|
|
40
|
+
onSystemMessage?: (message: string) => void;
|
|
41
|
+
/**
|
|
42
|
+
* How long to wait before the arming reconcile pass looks for a write the OS stream
|
|
43
|
+
* may have missed because it was not armed yet (see `stamp` below). 0 disables it;
|
|
44
|
+
* tests set it low, the default is tuned for a real save landing right at session start.
|
|
45
|
+
*/
|
|
46
|
+
armGraceMs?: number;
|
|
47
|
+
}): FileChangedWatcher | null;
|
|
48
|
+
/**
|
|
49
|
+
* Which watched files differ from the snapshot taken when the watcher armed. This is the
|
|
50
|
+
* whole substance of the arming reconcile: fs.watch arms asynchronously, so a write that
|
|
51
|
+
* lands before the stream is listening is not queued — it is gone. Exported so the
|
|
52
|
+
* comparison can be tested without racing an OS event.
|
|
53
|
+
*/
|
|
54
|
+
export declare function filesChangedSinceSnapshot(names: Iterable<string>, seen: Map<string, string>, stamp: (name: string) => string): string[];
|
|
55
|
+
//# sourceMappingURL=lifecycleHost.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycleHost.d.ts","sourceRoot":"","sources":["../../src/agent/lifecycleHost.ts"],"names":[],"mappings":"AAeA,kFAAkF;AAClF,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;IACtD,oHAAoH;IACpH,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,iBAAiB,EAAE,GACzB,OAAO,CAAC,IAAI,CAAC,CAef;AAED,MAAM,WAAW,kBAAkB;IACjC,6EAA6E;IAC7E,IAAI,IAAI,IAAI,CAAC;IACb,8FAA8F;IAC9F,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACxC,wFAAwF;IACxF,YAAY,IAAI,MAAM,EAAE,CAAC;CAC1B;AASD;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE;IACL,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GACA,kBAAkB,GAAG,IAAI,CA+H3B;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,EACvB,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EACzB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAC9B,MAAM,EAAE,CAIV"}
|