typeclaw 0.28.2 → 0.29.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/package.json +1 -1
- package/src/agent/index.ts +37 -5
- package/src/agent/loop-guard.ts +112 -26
- package/src/agent/plugin-tools.ts +102 -41
- package/src/agent/session-origin.ts +3 -3
- package/src/agent/subagents.ts +7 -0
- package/src/agent/system-prompt.ts +29 -4
- package/src/agent/tools/channel-send.ts +1 -1
- package/src/agent/tools/spawn-subagent.ts +21 -0
- package/src/agent/tools/subagent-output.ts +7 -3
- package/src/agent/tools/wikipedia.ts +1 -1
- package/src/bundled-plugins/explorer/explorer.ts +2 -0
- package/src/bundled-plugins/github-cli-auth/approve-idempotency.ts +74 -0
- package/src/bundled-plugins/github-cli-auth/effective-approval.ts +98 -0
- package/src/bundled-plugins/github-cli-auth/gh-review-inline-detect.ts +130 -0
- package/src/bundled-plugins/github-cli-auth/index.ts +27 -2
- package/src/bundled-plugins/github-cli-auth/review-recorder.ts +12 -4
- package/src/bundled-plugins/memory/memory-logger.ts +3 -3
- package/src/bundled-plugins/operator/operator.ts +2 -0
- package/src/bundled-plugins/planner/index.ts +11 -0
- package/src/bundled-plugins/planner/planner.ts +282 -0
- package/src/bundled-plugins/planner/skills/general.ts +65 -0
- package/src/bundled-plugins/planner/skills/project.ts +69 -0
- package/src/bundled-plugins/researcher/index.ts +11 -0
- package/src/bundled-plugins/researcher/researcher.ts +226 -0
- package/src/bundled-plugins/researcher/skills/general.ts +105 -0
- package/src/bundled-plugins/researcher/write-report.ts +107 -0
- package/src/bundled-plugins/reviewer/reviewer.ts +26 -8
- package/src/bundled-plugins/reviewer/skills/data-review.ts +77 -0
- package/src/bundled-plugins/reviewer/skills/doc-review.ts +79 -0
- package/src/bundled-plugins/reviewer/skills/plan-review.ts +64 -0
- package/src/bundled-plugins/reviewer/skills/security-audit.ts +70 -0
- package/src/bundled-plugins/reviewer/skills/writing-review.ts +63 -0
- package/src/bundled-plugins/scout/scout.ts +2 -0
- package/src/bundled-plugins/security/policies/prompt-injection.ts +8 -4
- package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +3 -2
- package/src/channels/adapters/discord-bot.ts +38 -11
- package/src/channels/adapters/github/inbound.ts +68 -4
- package/src/channels/adapters/kakaotalk-classify.ts +2 -2
- package/src/channels/adapters/kakaotalk.ts +2 -2
- package/src/channels/adapters/slack-bot-classify.ts +1 -1
- package/src/channels/adapters/slack-bot.ts +3 -0
- package/src/channels/adapters/telegram-bot.ts +3 -0
- package/src/channels/engagement.ts +12 -7
- package/src/channels/router.ts +32 -9
- package/src/channels/schema.ts +1 -1
- package/src/channels/types.ts +6 -0
- package/src/cli/init.ts +13 -2
- package/src/cli/ui.ts +64 -0
- package/src/config/config.ts +21 -15
- package/src/container/start.ts +5 -1
- package/src/init/dockerfile.ts +19 -56
- package/src/init/hatching.ts +1 -1
- package/src/init/index.ts +5 -1
- package/src/run/bundled-plugins.ts +4 -0
- package/src/server/index.ts +24 -5
- package/src/shared/host-locale.ts +27 -0
- package/src/shared/protocol.ts +1 -1
- package/src/shared/wordmark.ts +19 -0
- package/src/skills/typeclaw-config/SKILL.md +32 -32
- package/src/skills/typeclaw-kaomoji/SKILL.md +3 -3
- package/src/skills/typeclaw-tunnels/SKILL.md +3 -1
- package/src/tui/banner.ts +19 -0
- package/src/tui/format.ts +34 -0
- package/src/tui/index.ts +121 -22
- package/src/tui/theme.ts +26 -1
- package/src/tunnels/providers/cloudflare-named.ts +15 -4
- package/src/tunnels/providers/cloudflare-quick.ts +15 -4
- package/src/tunnels/providers/cloudflared-binary.ts +11 -0
- package/typeclaw.schema.json +15 -7
package/package.json
CHANGED
package/src/agent/index.ts
CHANGED
|
@@ -53,7 +53,12 @@ import { loadSelf } from './self'
|
|
|
53
53
|
import { SESSION_META_CUSTOM_TYPE, sessionMetaPayload } from './session-meta'
|
|
54
54
|
import { renderSessionOrigin, type SessionOrigin, type SessionRoleContext } from './session-origin'
|
|
55
55
|
import type { CreateSessionForSubagent, SubagentRegistry } from './subagents'
|
|
56
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
buildDefaultSystemPrompt,
|
|
58
|
+
DEFAULT_SUBAGENT_ROSTER,
|
|
59
|
+
renderRuntimeBlock,
|
|
60
|
+
SLIM_SYSTEM_PROMPT,
|
|
61
|
+
} from './system-prompt'
|
|
57
62
|
import { attachToolNotFoundNudge } from './tool-not-found-nudge'
|
|
58
63
|
import {
|
|
59
64
|
createBudgetState,
|
|
@@ -69,7 +74,7 @@ import { createChannelSendTool } from './tools/channel-send'
|
|
|
69
74
|
import { createGrantRoleTool } from './tools/grant-role'
|
|
70
75
|
import { createRestartTool } from './tools/restart'
|
|
71
76
|
import { createSkipResponseTool } from './tools/skip-response'
|
|
72
|
-
import { createSpawnSubagentTool } from './tools/spawn-subagent'
|
|
77
|
+
import { createSpawnSubagentTool, renderPublicSubagentRoster } from './tools/spawn-subagent'
|
|
73
78
|
import { createStreamSnapshotTool } from './tools/stream-snapshot'
|
|
74
79
|
import { createSubagentCancelTool } from './tools/subagent-cancel'
|
|
75
80
|
import { createSubagentOutputTool } from './tools/subagent-output'
|
|
@@ -256,6 +261,7 @@ export async function createSessionWithDispose(options: CreateSessionOptions = {
|
|
|
256
261
|
...(options.permissions ? { permissions: options.permissions } : {}),
|
|
257
262
|
...(options.runtimeVersion !== undefined ? { runtimeVersion: options.runtimeVersion } : {}),
|
|
258
263
|
...(options.mcpManager !== undefined ? { mcpManager: options.mcpManager } : {}),
|
|
264
|
+
...(options.subagentRegistry !== undefined ? { subagentRegistry: options.subagentRegistry } : {}),
|
|
259
265
|
})
|
|
260
266
|
|
|
261
267
|
const getOrigin: () => SessionOrigin | undefined =
|
|
@@ -580,7 +586,7 @@ export function formatRestartNotice(restartedAt: string): string {
|
|
|
580
586
|
|
|
581
587
|
// Variant for the session that called the `restart` tool. The user explicitly
|
|
582
588
|
// asked this conversation to restart; staying silent after the reboot is the
|
|
583
|
-
// reported bug ("
|
|
589
|
+
// reported bug (e.g. "wait, you don't even know you restarted?"). This notice instructs the
|
|
584
590
|
// model to acknowledge restart completion in its very next reply — once — then
|
|
585
591
|
// stop mentioning it. Same SYSTEM MESSAGE framing as the sibling notice so
|
|
586
592
|
// persona-rich models don't reply to the framing itself.
|
|
@@ -899,6 +905,12 @@ export type CreateResourceLoaderOptions = {
|
|
|
899
905
|
mcpManager?: McpManager
|
|
900
906
|
permissions?: PermissionService
|
|
901
907
|
runtimeVersion?: string
|
|
908
|
+
// Public subagents whose names + `rosterDescription`s render the full-mode
|
|
909
|
+
// "## Subagent orchestration" roster. When omitted (no-registry callers, the
|
|
910
|
+
// debug dumper), the prompt falls back to `DEFAULT_SUBAGENT_ROSTER`. Threaded
|
|
911
|
+
// from `createSessionWithDispose`, where the merged registry is already in
|
|
912
|
+
// scope.
|
|
913
|
+
subagentRegistry?: SubagentRegistry
|
|
902
914
|
// Explicit override for the prompt mode. When omitted, the mode is derived
|
|
903
915
|
// from `origin.kind`: cron + subagent → slim, tui + channel → full. Pass
|
|
904
916
|
// 'full' to force the heavy prompt even on an unattended origin (rarely
|
|
@@ -957,6 +969,11 @@ export type SystemPromptMode = 'full' | 'slim'
|
|
|
957
969
|
export type SystemPromptComposition = {
|
|
958
970
|
mode?: SystemPromptMode
|
|
959
971
|
self: string
|
|
972
|
+
// Pre-rendered full-mode orchestration roster (from `renderPublicSubagentRoster`).
|
|
973
|
+
// Kept as a ready string so this composer stays pure and registry-free; the
|
|
974
|
+
// registry-aware caller renders it. Ignored in slim mode (no roster section).
|
|
975
|
+
// Falls back to `DEFAULT_SUBAGENT_ROSTER` when omitted.
|
|
976
|
+
subagentRoster?: string
|
|
960
977
|
runtimeVersion?: string
|
|
961
978
|
origin?: SessionOrigin
|
|
962
979
|
roleContext?: SessionRoleContext
|
|
@@ -990,7 +1007,10 @@ export type SystemPromptComposition = {
|
|
|
990
1007
|
// suffix anyway — and removes the staleness failure mode where a session
|
|
991
1008
|
// opened Friday answered "today is Friday" on Thursday.
|
|
992
1009
|
export function composeSystemPrompt(parts: SystemPromptComposition): string {
|
|
993
|
-
const base =
|
|
1010
|
+
const base =
|
|
1011
|
+
parts.mode === 'slim'
|
|
1012
|
+
? SLIM_SYSTEM_PROMPT
|
|
1013
|
+
: buildDefaultSystemPrompt(parts.subagentRoster ?? DEFAULT_SUBAGENT_ROSTER)
|
|
994
1014
|
let prompt = `${base}\n\n${parts.self}`
|
|
995
1015
|
if (parts.runtimeVersion !== undefined) {
|
|
996
1016
|
prompt = `${prompt}\n\n${renderRuntimeBlock(parts.runtimeVersion)}`
|
|
@@ -1013,7 +1033,18 @@ export function composeSystemPrompt(parts: SystemPromptComposition): string {
|
|
|
1013
1033
|
export async function createResourceLoader(options: CreateResourceLoaderOptions = {}): Promise<DefaultResourceLoader> {
|
|
1014
1034
|
const agentDir = options.agentDir ?? process.cwd()
|
|
1015
1035
|
const mode: SystemPromptMode = options.mode ?? deriveSystemPromptMode(options.origin)
|
|
1016
|
-
|
|
1036
|
+
// Slim mode (cron/subagent) has no orchestration section, so it never reads
|
|
1037
|
+
// the roster. Skip rendering it there — `renderPublicSubagentRoster` throws on
|
|
1038
|
+
// a public subagent with a missing/blank `rosterDescription`, and a slim
|
|
1039
|
+
// session must not fail on a roster it will never show.
|
|
1040
|
+
const subagentRoster =
|
|
1041
|
+
mode === 'slim'
|
|
1042
|
+
? undefined
|
|
1043
|
+
: options.subagentRegistry !== undefined
|
|
1044
|
+
? renderPublicSubagentRoster(options.subagentRegistry)
|
|
1045
|
+
: DEFAULT_SUBAGENT_ROSTER
|
|
1046
|
+
const basePrompt =
|
|
1047
|
+
mode === 'slim' ? SLIM_SYSTEM_PROMPT : buildDefaultSystemPrompt(subagentRoster ?? DEFAULT_SUBAGENT_ROSTER)
|
|
1017
1048
|
|
|
1018
1049
|
// Kick off the three independent I/O paths concurrently. Sequential awaits
|
|
1019
1050
|
// here used to be the dominant cold-start cost amplifier: loadSelf is 2
|
|
@@ -1077,6 +1108,7 @@ export async function createResourceLoader(options: CreateResourceLoaderOptions
|
|
|
1077
1108
|
const systemPrompt = composeSystemPrompt({
|
|
1078
1109
|
mode,
|
|
1079
1110
|
self,
|
|
1111
|
+
subagentRoster,
|
|
1080
1112
|
...(options.runtimeVersion !== undefined ? { runtimeVersion: options.runtimeVersion } : {}),
|
|
1081
1113
|
...(options.origin !== undefined ? { origin: options.origin } : {}),
|
|
1082
1114
|
...(roleContext !== undefined ? { roleContext } : {}),
|
package/src/agent/loop-guard.ts
CHANGED
|
@@ -52,9 +52,40 @@ const DEFAULT_PATH_TARGET = '.'
|
|
|
52
52
|
|
|
53
53
|
const MAX_SESSIONS = 256
|
|
54
54
|
|
|
55
|
+
// The one tool with result-sensitive loop semantics: a poll returning 'running'
|
|
56
|
+
// is a legitimate wait, so its block is deferred until status is known (see
|
|
57
|
+
// `noteResult` / `deferable`). Kept as a local literal rather than importing the
|
|
58
|
+
// tool module to keep this primitive dependency-free; it must match
|
|
59
|
+
// SUBAGENT_OUTPUT_TOOL_NAME in tools/subagent-output.ts.
|
|
60
|
+
const SUBAGENT_OUTPUT_TOOL = 'subagent_output'
|
|
61
|
+
|
|
55
62
|
export type LoopReason = 'consecutive' | 'windowed'
|
|
56
63
|
|
|
64
|
+
// Identifies the single observation a `check` recorded so a caller can retract
|
|
65
|
+
// exactly that one after learning post-execution it was not a loop (e.g. a
|
|
66
|
+
// `subagent_output` poll that returned `status: 'running'`). Narrower than
|
|
67
|
+
// `forgetTool`, which drops the whole tool window: retract undoes one call, so
|
|
68
|
+
// unrelated task_ids and terminal-result polls keep their accumulated signal.
|
|
69
|
+
export type LoopGuardReceipt = {
|
|
70
|
+
sessionId: string
|
|
71
|
+
tool: string
|
|
72
|
+
signature: string
|
|
73
|
+
windowSignature: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Post-execution classification of a `subagent_output` poll, fed back via
|
|
77
|
+
// `noteResult`. 'running' is a still-pending wait; 'terminal' is completed/failed
|
|
78
|
+
// — a repeated terminal poll is a real loop.
|
|
79
|
+
export type LoopObservedResult = 'running' | 'terminal'
|
|
80
|
+
|
|
57
81
|
export type LoopGuardDecision =
|
|
82
|
+
| { kind: 'ok'; receipt: LoopGuardReceipt }
|
|
83
|
+
| { kind: 'warn'; count: number; reason: LoopReason; message: string; receipt: LoopGuardReceipt }
|
|
84
|
+
| { kind: 'block'; count: number; reason: LoopReason; message: string; receipt: LoopGuardReceipt; deferable: boolean }
|
|
85
|
+
|
|
86
|
+
// A decision before its receipt is attached. The detector helpers produce these;
|
|
87
|
+
// `check` stamps the receipt on at its single return site.
|
|
88
|
+
type Verdict =
|
|
58
89
|
| { kind: 'ok' }
|
|
59
90
|
| { kind: 'warn'; count: number; reason: LoopReason; message: string }
|
|
60
91
|
| { kind: 'block'; count: number; reason: LoopReason; message: string }
|
|
@@ -71,6 +102,21 @@ export type LoopGuard = {
|
|
|
71
102
|
// premature polls poisoned the window. Narrower than `forget`, so an
|
|
72
103
|
// unrelated tool's accumulating loop on the same session is preserved.
|
|
73
104
|
forgetTool: (sessionId: string, tool: string) => void
|
|
105
|
+
// Undoes the one observation a prior `check` recorded, identified by its
|
|
106
|
+
// receipt. Pops that signature from the windowed history and, when the
|
|
107
|
+
// current consecutive streak is the call this receipt named (it is the most
|
|
108
|
+
// recent `check` on the session, since tool execution within a turn is
|
|
109
|
+
// sequential), rewinds the streak by one. Used post-execution for a
|
|
110
|
+
// `subagent_output` poll that returned `status: 'running'` — a still-pending
|
|
111
|
+
// wait, not a loop — so it never accumulates toward either detector.
|
|
112
|
+
retract: (receipt: LoopGuardReceipt) => void
|
|
113
|
+
// Records the post-execution class of a `subagent_output` poll. Once a
|
|
114
|
+
// signature is seen 'terminal', `check` stops marking its blocks `deferable`,
|
|
115
|
+
// so further identical polls hard-block PRE-execute instead of running again
|
|
116
|
+
// just to re-confirm a completed task. 'running' clears any prior terminal
|
|
117
|
+
// mark for that signature (a task can only move running→terminal, but a
|
|
118
|
+
// signature can be reused across episodes).
|
|
119
|
+
noteResult: (receipt: LoopGuardReceipt, result: LoopObservedResult) => void
|
|
74
120
|
}
|
|
75
121
|
|
|
76
122
|
type SessionState = {
|
|
@@ -84,6 +130,10 @@ type SessionState = {
|
|
|
84
130
|
// still present in the window.
|
|
85
131
|
window: string[]
|
|
86
132
|
windowWarned: Set<string>
|
|
133
|
+
// Exact signatures whose `subagent_output` poll has been observed terminal.
|
|
134
|
+
// A block on such a signature is enforced pre-execute (not deferred), so a
|
|
135
|
+
// completed task is not re-polled forever just to re-learn it is done.
|
|
136
|
+
termKnown: Set<string>
|
|
87
137
|
}
|
|
88
138
|
|
|
89
139
|
export type CreateLoopGuardOptions = {
|
|
@@ -134,7 +184,7 @@ export function createLoopGuard(options: CreateLoopGuardOptions = {}): LoopGuard
|
|
|
134
184
|
}
|
|
135
185
|
}
|
|
136
186
|
|
|
137
|
-
function evaluateConsecutive(state: SessionState, tool: string):
|
|
187
|
+
function evaluateConsecutive(state: SessionState, tool: string): Verdict {
|
|
138
188
|
if (state.count >= hardBlock) {
|
|
139
189
|
return {
|
|
140
190
|
kind: 'block',
|
|
@@ -150,32 +200,38 @@ export function createLoopGuard(options: CreateLoopGuardOptions = {}): LoopGuard
|
|
|
150
200
|
return { kind: 'ok' }
|
|
151
201
|
}
|
|
152
202
|
|
|
153
|
-
function evaluateWindowed(state: SessionState, tool: string, windowSig: string):
|
|
203
|
+
function evaluateWindowed(state: SessionState, tool: string, windowSig: string): Verdict {
|
|
154
204
|
const count = state.window.reduce((n, sig) => (sig === windowSig ? n + 1 : n), 0)
|
|
155
205
|
if (count >= windowHardBlock) {
|
|
156
|
-
return {
|
|
157
|
-
kind: 'block',
|
|
158
|
-
count,
|
|
159
|
-
reason: 'windowed',
|
|
160
|
-
message: formatWindowedBlockMessage(tool, count),
|
|
161
|
-
}
|
|
206
|
+
return { kind: 'block', count, reason: 'windowed', message: formatWindowedBlockMessage(tool, count) }
|
|
162
207
|
}
|
|
163
208
|
if (count >= windowSoftWarn && !state.windowWarned.has(windowSig)) {
|
|
164
209
|
state.windowWarned.add(windowSig)
|
|
165
|
-
return {
|
|
166
|
-
kind: 'warn',
|
|
167
|
-
count,
|
|
168
|
-
reason: 'windowed',
|
|
169
|
-
message: formatWindowedWarnMessage(tool, count),
|
|
170
|
-
}
|
|
210
|
+
return { kind: 'warn', count, reason: 'windowed', message: formatWindowedWarnMessage(tool, count) }
|
|
171
211
|
}
|
|
172
212
|
return { kind: 'ok' }
|
|
173
213
|
}
|
|
174
214
|
|
|
215
|
+
function resolveVerdict(state: SessionState, tool: string, windowSig: string): Verdict {
|
|
216
|
+
const consecutive = evaluateConsecutive(state, tool)
|
|
217
|
+
if (consecutive.kind === 'block') return consecutive
|
|
218
|
+
|
|
219
|
+
// Back-to-back identical calls are the consecutive detector's domain; let
|
|
220
|
+
// it own them so a tight streak doesn't also trip the windowed detector.
|
|
221
|
+
// The windowed detector exists for INTERLEAVED cycles, so it only acts
|
|
222
|
+
// when this call breaks the immediate streak (count === 1).
|
|
223
|
+
const windowed = state.count === 1 ? evaluateWindowed(state, tool, windowSig) : { kind: 'ok' as const }
|
|
224
|
+
if (windowed.kind === 'block') return windowed
|
|
225
|
+
if (consecutive.kind === 'warn') return consecutive
|
|
226
|
+
if (windowed.kind === 'warn') return windowed
|
|
227
|
+
return { kind: 'ok' }
|
|
228
|
+
}
|
|
229
|
+
|
|
175
230
|
return {
|
|
176
231
|
check(sessionId, tool, args) {
|
|
177
232
|
const signature = makeCallSignature(tool, args)
|
|
178
233
|
const windowSig = makeWindowSignature(tool, args)
|
|
234
|
+
const receipt: LoopGuardReceipt = { sessionId, tool, signature, windowSignature: windowSig }
|
|
179
235
|
const existing = sessions.get(sessionId)
|
|
180
236
|
|
|
181
237
|
const state: SessionState = existing ?? {
|
|
@@ -184,6 +240,7 @@ export function createLoopGuard(options: CreateLoopGuardOptions = {}): LoopGuard
|
|
|
184
240
|
warned: false,
|
|
185
241
|
window: [],
|
|
186
242
|
windowWarned: new Set(),
|
|
243
|
+
termKnown: new Set(),
|
|
187
244
|
}
|
|
188
245
|
|
|
189
246
|
if (state.signature !== signature) {
|
|
@@ -204,18 +261,14 @@ export function createLoopGuard(options: CreateLoopGuardOptions = {}): LoopGuard
|
|
|
204
261
|
|
|
205
262
|
touch(sessionId, state)
|
|
206
263
|
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (windowed.kind === 'block') return windowed
|
|
216
|
-
if (consecutive.kind === 'warn') return consecutive
|
|
217
|
-
if (windowed.kind === 'warn') return windowed
|
|
218
|
-
return { kind: 'ok' }
|
|
264
|
+
const verdict = resolveVerdict(state, tool, windowSig)
|
|
265
|
+
if (verdict.kind === 'block') {
|
|
266
|
+
// A `subagent_output` block is deferable (let the boundary call execute
|
|
267
|
+
// to learn its status) only until this signature has proven terminal.
|
|
268
|
+
const deferable = tool === SUBAGENT_OUTPUT_TOOL && !state.termKnown.has(signature)
|
|
269
|
+
return { ...verdict, receipt, deferable }
|
|
270
|
+
}
|
|
271
|
+
return { ...verdict, receipt }
|
|
219
272
|
},
|
|
220
273
|
reset(sessionId) {
|
|
221
274
|
sessions.delete(sessionId)
|
|
@@ -240,6 +293,39 @@ export function createLoopGuard(options: CreateLoopGuardOptions = {}): LoopGuard
|
|
|
240
293
|
state.count = 0
|
|
241
294
|
state.warned = false
|
|
242
295
|
}
|
|
296
|
+
for (const sig of state.termKnown) {
|
|
297
|
+
if (signatureBelongsToTool(sig, tool)) state.termKnown.delete(sig)
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
retract(receipt) {
|
|
301
|
+
const state = sessions.get(receipt.sessionId)
|
|
302
|
+
if (state === undefined) return
|
|
303
|
+
|
|
304
|
+
// Pop the receipt's windowed observation. It is the most recent push for
|
|
305
|
+
// this signature (retraction runs immediately after the call's execute,
|
|
306
|
+
// before any other tool runs on the session), so remove the last match.
|
|
307
|
+
const lastIdx = state.window.lastIndexOf(receipt.windowSignature)
|
|
308
|
+
if (lastIdx !== -1) {
|
|
309
|
+
state.window.splice(lastIdx, 1)
|
|
310
|
+
if (!state.window.includes(receipt.windowSignature)) {
|
|
311
|
+
state.windowWarned.delete(receipt.windowSignature)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Rewind the consecutive streak by one only if it is still the call this
|
|
316
|
+
// receipt named. A retracted soft-warned streak re-arms its warning so the
|
|
317
|
+
// next genuine repeat warns as if this call never happened.
|
|
318
|
+
if (state.signature === receipt.signature && state.count > 0) {
|
|
319
|
+
state.count -= 1
|
|
320
|
+
if (state.count < softWarn) state.warned = false
|
|
321
|
+
if (state.count === 0) state.signature = ''
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
noteResult(receipt, result) {
|
|
325
|
+
const state = sessions.get(receipt.sessionId)
|
|
326
|
+
if (state === undefined) return
|
|
327
|
+
if (result === 'terminal') state.termKnown.add(receipt.signature)
|
|
328
|
+
else state.termKnown.delete(receipt.signature)
|
|
243
329
|
},
|
|
244
330
|
}
|
|
245
331
|
}
|
|
@@ -45,9 +45,10 @@ import {
|
|
|
45
45
|
subtractMasked,
|
|
46
46
|
} from '@/sandbox'
|
|
47
47
|
|
|
48
|
-
import { createLoopGuard, type LoopGuard } from './loop-guard'
|
|
48
|
+
import { createLoopGuard, type LoopGuard, type LoopGuardDecision } from './loop-guard'
|
|
49
49
|
import { checkImageReadRedirect } from './multimodal/read-redirect'
|
|
50
50
|
import type { SessionOrigin } from './session-origin'
|
|
51
|
+
import { SUBAGENT_OUTPUT_TOOL_NAME, type SubagentOutputToolDetails } from './tools/subagent-output'
|
|
51
52
|
import { webFetchTool } from './tools/webfetch'
|
|
52
53
|
import { webSearchTool } from './tools/websearch'
|
|
53
54
|
|
|
@@ -241,10 +242,10 @@ export function wrapPluginTool(tool: Tool<any>, opts: WrapToolOptions): ToolDefi
|
|
|
241
242
|
return errorResult(`blocked: ${blockResult.reason}`)
|
|
242
243
|
}
|
|
243
244
|
|
|
244
|
-
const
|
|
245
|
-
if (
|
|
245
|
+
const loopGate = gateLoopGuard(opts.sessionId, opts.toolName, before.args)
|
|
246
|
+
if (loopGate.blockNow) {
|
|
246
247
|
fireLoopAbort(opts.getAbort)
|
|
247
|
-
return errorResult(
|
|
248
|
+
return errorResult(loopGate.message)
|
|
248
249
|
}
|
|
249
250
|
|
|
250
251
|
const toolCtx: ToolContext = {
|
|
@@ -262,9 +263,12 @@ export function wrapPluginTool(tool: Tool<any>, opts: WrapToolOptions): ToolDefi
|
|
|
262
263
|
return errorResult(message)
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
|
|
266
|
-
|
|
266
|
+
const resolved = loopGate.resolve(result)
|
|
267
|
+
if ('deferredBlock' in resolved) {
|
|
268
|
+
fireLoopAbort(opts.getAbort)
|
|
269
|
+
return errorResult(resolved.deferredBlock)
|
|
267
270
|
}
|
|
271
|
+
result = resolved.result
|
|
268
272
|
|
|
269
273
|
await opts.hooks.runToolAfter({
|
|
270
274
|
tool: opts.toolName,
|
|
@@ -301,10 +305,10 @@ export function wrapSystemTool<TParams extends TSchema, TDetails = unknown, TSta
|
|
|
301
305
|
if (blockResult !== undefined) {
|
|
302
306
|
throw new Error(`blocked: ${blockResult.reason}`)
|
|
303
307
|
}
|
|
304
|
-
const
|
|
305
|
-
if (
|
|
308
|
+
const loopGate = gateLoopGuard(opts.sessionId, tool.name, mutableArgs)
|
|
309
|
+
if (loopGate.blockNow) {
|
|
306
310
|
fireLoopAbort(opts.getAbort)
|
|
307
|
-
throw new Error(
|
|
311
|
+
throw new Error(loopGate.message)
|
|
308
312
|
}
|
|
309
313
|
const guardResult = await runFinalWriteGuards({
|
|
310
314
|
tool: tool.name,
|
|
@@ -321,15 +325,12 @@ export function wrapSystemTool<TParams extends TSchema, TDetails = unknown, TSta
|
|
|
321
325
|
stripGuardAcknowledgements(mutableArgs)
|
|
322
326
|
|
|
323
327
|
const result = await tool.execute(toolCallId, mutableArgs as Static<TParams>, signal, onUpdate, ctx)
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (loopDecision.kind === 'warn') {
|
|
329
|
-
const warned = appendLoopWarning(hookResult, loopDecision.message)
|
|
330
|
-
hookResult.content = warned.content
|
|
331
|
-
hookResult.details = warned.details
|
|
328
|
+
const resolved = loopGate.resolve({ content: result.content as ContentPart[], details: result.details })
|
|
329
|
+
if ('deferredBlock' in resolved) {
|
|
330
|
+
fireLoopAbort(opts.getAbort)
|
|
331
|
+
throw new Error(resolved.deferredBlock)
|
|
332
332
|
}
|
|
333
|
+
const hookResult = resolved.result
|
|
333
334
|
await opts.hooks.runToolAfter({
|
|
334
335
|
tool: tool.name,
|
|
335
336
|
sessionId: opts.sessionId,
|
|
@@ -337,7 +338,7 @@ export function wrapSystemTool<TParams extends TSchema, TDetails = unknown, TSta
|
|
|
337
338
|
result: hookResult,
|
|
338
339
|
})
|
|
339
340
|
return {
|
|
340
|
-
content: hookResult.content,
|
|
341
|
+
content: hookResult.content as ContentPart[],
|
|
341
342
|
details: hookResult.details as TDetails,
|
|
342
343
|
}
|
|
343
344
|
},
|
|
@@ -364,10 +365,10 @@ export function wrapSystemAgentTool<TParams extends TSchema, TDetails = unknown>
|
|
|
364
365
|
if (blockResult !== undefined) {
|
|
365
366
|
throw new Error(`blocked: ${blockResult.reason}`)
|
|
366
367
|
}
|
|
367
|
-
const
|
|
368
|
-
if (
|
|
368
|
+
const loopGate = gateLoopGuard(opts.sessionId, tool.name, mutableArgs)
|
|
369
|
+
if (loopGate.blockNow) {
|
|
369
370
|
fireLoopAbort(opts.getAbort)
|
|
370
|
-
throw new Error(
|
|
371
|
+
throw new Error(loopGate.message)
|
|
371
372
|
}
|
|
372
373
|
const guardResult = await runFinalWriteGuards({
|
|
373
374
|
tool: tool.name,
|
|
@@ -384,15 +385,12 @@ export function wrapSystemAgentTool<TParams extends TSchema, TDetails = unknown>
|
|
|
384
385
|
stripGuardAcknowledgements(mutableArgs)
|
|
385
386
|
|
|
386
387
|
const result = await tool.execute(toolCallId, mutableArgs as Static<TParams>, signal, onUpdate)
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (loopDecision.kind === 'warn') {
|
|
392
|
-
const warned = appendLoopWarning(hookResult, loopDecision.message)
|
|
393
|
-
hookResult.content = warned.content
|
|
394
|
-
hookResult.details = warned.details
|
|
388
|
+
const resolved = loopGate.resolve({ content: result.content as ContentPart[], details: result.details })
|
|
389
|
+
if ('deferredBlock' in resolved) {
|
|
390
|
+
fireLoopAbort(opts.getAbort)
|
|
391
|
+
throw new Error(resolved.deferredBlock)
|
|
395
392
|
}
|
|
393
|
+
const hookResult = resolved.result
|
|
396
394
|
await opts.hooks.runToolAfter({
|
|
397
395
|
tool: tool.name,
|
|
398
396
|
sessionId: opts.sessionId,
|
|
@@ -400,7 +398,7 @@ export function wrapSystemAgentTool<TParams extends TSchema, TDetails = unknown>
|
|
|
400
398
|
result: hookResult,
|
|
401
399
|
})
|
|
402
400
|
return {
|
|
403
|
-
content: hookResult.content,
|
|
401
|
+
content: hookResult.content as ContentPart[],
|
|
404
402
|
details: hookResult.details as TDetails,
|
|
405
403
|
}
|
|
406
404
|
},
|
|
@@ -442,10 +440,10 @@ export function wrapAgentToolAsCustomToolDefinition<TParams extends TSchema, TDe
|
|
|
442
440
|
// loop-detection state, or pi's execute.
|
|
443
441
|
const bashEnvOverlay = readBashEnvOverlay(mutableArgs)
|
|
444
442
|
delete mutableArgs[TYPECLAW_INTERNAL_BASH_ENV]
|
|
445
|
-
const
|
|
446
|
-
if (
|
|
443
|
+
const loopGate = gateLoopGuard(opts.sessionId, tool.name, mutableArgs)
|
|
444
|
+
if (loopGate.blockNow) {
|
|
447
445
|
fireLoopAbort(opts.getAbort)
|
|
448
|
-
throw new Error(
|
|
446
|
+
throw new Error(loopGate.message)
|
|
449
447
|
}
|
|
450
448
|
const guardResult = await runFinalWriteGuards({
|
|
451
449
|
tool: tool.name,
|
|
@@ -472,15 +470,12 @@ export function wrapAgentToolAsCustomToolDefinition<TParams extends TSchema, TDe
|
|
|
472
470
|
const result = await bashEnvStore.run(bashEnvOverlay, () =>
|
|
473
471
|
tool.execute(toolCallId, mutableArgs as Static<TParams>, signal, onUpdate),
|
|
474
472
|
)
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
if (loopDecision.kind === 'warn') {
|
|
480
|
-
const warned = appendLoopWarning(hookResult, loopDecision.message)
|
|
481
|
-
hookResult.content = warned.content
|
|
482
|
-
hookResult.details = warned.details
|
|
473
|
+
const resolved = loopGate.resolve({ content: result.content as ContentPart[], details: result.details })
|
|
474
|
+
if ('deferredBlock' in resolved) {
|
|
475
|
+
fireLoopAbort(opts.getAbort)
|
|
476
|
+
throw new Error(resolved.deferredBlock)
|
|
483
477
|
}
|
|
478
|
+
const hookResult = resolved.result
|
|
484
479
|
await opts.hooks.runToolAfter({
|
|
485
480
|
tool: tool.name,
|
|
486
481
|
sessionId: opts.sessionId,
|
|
@@ -609,6 +604,72 @@ function appendLoopWarning(result: ToolResult, message: string): ToolResult {
|
|
|
609
604
|
return { content, details: result.details }
|
|
610
605
|
}
|
|
611
606
|
|
|
607
|
+
// `subagent_output` is a read-only poll whose loop/no-loop classification only
|
|
608
|
+
// becomes knowable AFTER execution: a result of `status: 'running'` is a
|
|
609
|
+
// still-pending wait (legitimate), while a repeated terminal result is a real
|
|
610
|
+
// loop. The loop guard's `check` is result-blind and pre-execution, so for this
|
|
611
|
+
// one tool we DEFER enforcing a block until the status is known — otherwise the
|
|
612
|
+
// exact poll that would reveal 'running' gets blocked before it can run (the
|
|
613
|
+
// boundary-call hazard for round-robin fan-out polling). Every other tool
|
|
614
|
+
// enforces its block immediately, as before.
|
|
615
|
+
// A block is deferred only for a `subagent_output` poll the guard still marks
|
|
616
|
+
// `deferable` — i.e. whose signature has not yet proven terminal. Once a poll of
|
|
617
|
+
// that signature returns completed/failed, `deferable` is false and the block is
|
|
618
|
+
// enforced pre-execute, so a finished task is not re-polled forever.
|
|
619
|
+
function shouldDeferLoopBlock(toolName: string, decision: LoopGuardDecision): boolean {
|
|
620
|
+
return toolName === SUBAGENT_OUTPUT_TOOL_NAME && decision.kind === 'block' && decision.deferable
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function subagentPollStatus(toolName: string, result: ToolResult): 'running' | 'terminal' | undefined {
|
|
624
|
+
if (toolName !== SUBAGENT_OUTPUT_TOOL_NAME) return undefined
|
|
625
|
+
const details = result.details as SubagentOutputToolDetails | undefined
|
|
626
|
+
if (details?.ok !== true) return undefined
|
|
627
|
+
return details.status === 'running' ? 'running' : 'terminal'
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
type LoopGuardGate = {
|
|
631
|
+
// True when the guard wants to block AND the block is enforced now (every tool
|
|
632
|
+
// except a deferable `subagent_output` poll). The caller aborts + errors.
|
|
633
|
+
blockNow: boolean
|
|
634
|
+
message: string
|
|
635
|
+
// Resolves the guard against the tool's result. Returns the result to surface
|
|
636
|
+
// (possibly warn-annotated), or `{ deferredBlock: message }` when a deferred
|
|
637
|
+
// `subagent_output` block must now be enforced because the poll did not return
|
|
638
|
+
// a still-running status.
|
|
639
|
+
resolve: (result: ToolResult) => { result: ToolResult } | { deferredBlock: string }
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Single chokepoint for the loop-guard pre-check + post-execute resolution so
|
|
643
|
+
// all four tool wrappers share identical deferred-block / pending-retract
|
|
644
|
+
// semantics. `check` runs here (recording the observation); the returned
|
|
645
|
+
// `resolve` is called after execute with the tool's result, feeding the poll's
|
|
646
|
+
// running/terminal status back to the guard so future blocks stop deferring.
|
|
647
|
+
function gateLoopGuard(sessionId: string, toolName: string, args: unknown): LoopGuardGate {
|
|
648
|
+
const decision = sharedLoopGuard.check(sessionId, toolName, args)
|
|
649
|
+
const defer = shouldDeferLoopBlock(toolName, decision)
|
|
650
|
+
return {
|
|
651
|
+
blockNow: decision.kind === 'block' && !defer,
|
|
652
|
+
message: decision.kind === 'ok' ? '' : decision.message,
|
|
653
|
+
resolve(result) {
|
|
654
|
+
const pollStatus = subagentPollStatus(toolName, result)
|
|
655
|
+
if (pollStatus !== undefined) {
|
|
656
|
+
sharedLoopGuard.noteResult(decision.receipt, pollStatus)
|
|
657
|
+
}
|
|
658
|
+
if (pollStatus === 'running') {
|
|
659
|
+
sharedLoopGuard.retract(decision.receipt)
|
|
660
|
+
return { result }
|
|
661
|
+
}
|
|
662
|
+
if (defer && decision.kind === 'block') {
|
|
663
|
+
return { deferredBlock: decision.message }
|
|
664
|
+
}
|
|
665
|
+
if (decision.kind === 'warn') {
|
|
666
|
+
return { result: appendLoopWarning(result, decision.message) }
|
|
667
|
+
}
|
|
668
|
+
return { result }
|
|
669
|
+
},
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
612
673
|
// Clears one tool's loop-guard residue for a session on the process-wide shared
|
|
613
674
|
// guard. The completion-reminder bridges (channel router + TUI server) call this
|
|
614
675
|
// for `subagent_output` when a backgrounded subagent finishes, so the next fetch
|
|
@@ -630,9 +630,9 @@ function renderParticipants(
|
|
|
630
630
|
// mention syntax) and Telegram (uses `@username`, where `authorId` is a
|
|
631
631
|
// numeric id and NOT the username). See issue #188.
|
|
632
632
|
//
|
|
633
|
-
// Symptom in the wild before PR #183 + this fix:
|
|
634
|
-
// "
|
|
635
|
-
// check, so
|
|
633
|
+
// Symptom in the wild before PR #183 + this fix: Kiki addressing Momo as
|
|
634
|
+
// "Momo님" (plain text) on Discord, which never trips Momo's `isBotMention`
|
|
635
|
+
// check, so Momo observes silently and the conversation stalls. The
|
|
636
636
|
// angle-id branch here is exactly the fix for that case; the at-username
|
|
637
637
|
// and alias branches keep the platform contract honest for KakaoTalk and
|
|
638
638
|
// Telegram instead of self-contradicting the per-adapter mention guidance
|
package/src/agent/subagents.ts
CHANGED
|
@@ -48,6 +48,13 @@ export type SubagentShared<P = unknown> = {
|
|
|
48
48
|
handler?: (ctx: SubagentContext<P>, runSession: RunSession) => Promise<void>
|
|
49
49
|
toolResultBudget?: ToolResultBudget
|
|
50
50
|
visibility?: 'public' | 'internal'
|
|
51
|
+
// One-line purpose blurb for the main agent's "## Subagent orchestration"
|
|
52
|
+
// roster, rendered from the registry by `renderPublicSubagentRoster` instead
|
|
53
|
+
// of hand-maintained in the prompt (the drift that once left `researcher` and
|
|
54
|
+
// `planner` unlisted). Required for `visibility: 'public'`; ignored otherwise.
|
|
55
|
+
// On `SubagentShared` so the plugin→internal shim carries it via rest-spread
|
|
56
|
+
// (see `pluginSubagentShim`), like `visibility`.
|
|
57
|
+
rosterDescription?: string
|
|
51
58
|
requiresSpecificPermission?: boolean
|
|
52
59
|
// Opt-in: when true, this subagent's session is wired with the orchestration
|
|
53
60
|
// tools (spawn_subagent/subagent_output/subagent_cancel) so it can delegate
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { formatLocalDateTime, formatLocalWeekday, resolveLocalTimezoneName } from '@/shared'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// The orchestration roster (the `Briefly: ...` enumeration of public subagents)
|
|
4
|
+
// is GENERATED from the registry by `renderPublicSubagentRoster` and threaded in
|
|
5
|
+
// here, so a newly-registered public subagent can never be silently missing from
|
|
6
|
+
// the prompt — the drift that once left `researcher` and `planner` unlisted. The
|
|
7
|
+
// rest of the prompt is static. `DEFAULT_SUBAGENT_ROSTER` is the placeholder used
|
|
8
|
+
// by the no-registry path (back-compat callers, the debug dumper); production
|
|
9
|
+
// full-mode sessions pass the real registry-rendered roster via
|
|
10
|
+
// `composeSystemPrompt`'s `subagentRoster` field.
|
|
11
|
+
export function buildDefaultSystemPrompt(subagentRoster: string): string {
|
|
12
|
+
return `You are a general-purpose AI agent running inside TypeClaw.
|
|
4
13
|
|
|
5
14
|
TypeClaw is domain-agnostic — your purpose is defined by \`IDENTITY.md\`, your character by \`SOUL.md\`, and your operating manual by \`AGENTS.md\`. This system prompt only describes the runtime around you.
|
|
6
15
|
|
|
@@ -80,13 +89,13 @@ Your agent folder is a git repository.
|
|
|
80
89
|
|
|
81
90
|
## Subagent orchestration
|
|
82
91
|
|
|
83
|
-
Delegate focused work to subagents via \`spawn_subagent\`, \`subagent_output\`, \`subagent_cancel\`. Each runs in its own context window with its own tool set. The available subagents and their purpose are listed in the \`spawn_subagent\` tool description — re-read it before delegating. Briefly:
|
|
92
|
+
Delegate focused work to subagents via \`spawn_subagent\`, \`subagent_output\`, \`subagent_cancel\`. Each runs in its own context window with its own tool set. The available subagents and their purpose are listed in the \`spawn_subagent\` tool description — re-read it before delegating. Briefly: ${subagentRoster}.
|
|
84
93
|
|
|
85
94
|
There are three delegation modes. Pick deliberately.
|
|
86
95
|
|
|
87
|
-
**Mode A — Research fan-out.** Need information and the search is broad? Fire 2-5 subagents (usually \`explorer\`/\`scout\`) in parallel with \`run_in_background: true\`, then end your response. A \`<system-reminder>\` lands per completion; call \`subagent_output\` once per task_id to collect (it never blocks) and answer.
|
|
96
|
+
**Mode A — Research fan-out.** Need information and the search is broad? Fire 2-5 subagents (usually \`explorer\`/\`scout\`) in parallel with \`run_in_background: true\`, then end your response. A \`<system-reminder>\` lands per completion; call \`subagent_output\` once per task_id to collect (it never blocks) and answer. Match the worker to the depth: a fast or narrow web lookup goes to \`scout\`; a fuzzy question that needs decomposition, many sources, cross-validation, and a synthesized verdict goes to \`researcher\` (don't do that grind inline with \`web_search\` yourself).
|
|
88
97
|
|
|
89
|
-
**Mode B — Delegate-and-converse.** Asked to DO something long-running (>~30s: installs, builds, \`docker\`, scrapes, long test suites, multi-host loops, any noisy "fetch N and synthesize" chain)? Don't run it inline — blocking your own \`bash\` freezes the conversation and stalls the channel typing heartbeat (\`MAX_TYPING_HEARTBEAT_MS\`). Spawn one subagent (\`operator\` for side effects, \`scout\` for
|
|
98
|
+
**Mode B — Delegate-and-converse.** Asked to DO something long-running (>~30s: installs, builds, \`docker\`, scrapes, long test suites, multi-host loops, any noisy "fetch N and synthesize" chain)? Don't run it inline — blocking your own \`bash\` freezes the conversation and stalls the channel typing heartbeat (\`MAX_TYPING_HEARTBEAT_MS\`). Spawn one subagent (\`operator\` for side effects, \`scout\` for a quick web lookup, \`researcher\` for a deep multi-source "fetch N and synthesize" investigation, \`planner\` when a multi-step goal needs a sequenced, risk-aware plan before anyone acts) with \`run_in_background: true\`, acknowledge, and KEEP TALKING. Single fast calls (\`git status\`, one known-endpoint \`curl\`) stay inline. When the completion reminder lands, weave the result in; in a channel session, the completion \`<system-reminder>\` is NOT a user message but plain text is still invisible — Surface the result via \`channel_reply\` (or \`channel_send\`). If you already posted the substantive answer in the spawn turn, prefer \`skip_response({ reason: "result confirms prior reply" })\` over going silent.
|
|
90
99
|
|
|
91
100
|
**Mode C — Troubleshooting.** Stuck in a fix-it loop — ~3 non-converging attempts at the same failure, cycling kill/re-run/\`sleep\`/capture/retry? Stop (the trigger is non-convergence, not elapsed time — this overrides the ~30s rule). Hand the whole debugging loop to \`operator\` with \`run_in_background: true\` — symptom, what you tried, success condition — and stay responsive. Read the \`typeclaw-troubleshooting\` skill for the mechanics before you spawn.
|
|
92
101
|
|
|
@@ -109,6 +118,22 @@ You have no independent goals. Do not pursue self-preservation, replication, res
|
|
|
109
118
|
---
|
|
110
119
|
|
|
111
120
|
You are not pi, not Claude, not ChatGPT. You are the agent described by your own IDENTITY.md and SOUL.md. Let those files define your voice.`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Placeholder roster for the no-registry path: back-compat callers of
|
|
124
|
+
// `composeSystemPrompt`/`createResourceLoader` that pass no `subagentRoster`,
|
|
125
|
+
// and the debug dumper (which renders without a live registry). Production
|
|
126
|
+
// full-mode sessions always pass the real registry-rendered roster, so this
|
|
127
|
+
// text never reaches a real agent — it only keeps the standalone
|
|
128
|
+
// `DEFAULT_SYSTEM_PROMPT` constant a valid, self-contained string for tests.
|
|
129
|
+
export const DEFAULT_SUBAGENT_ROSTER =
|
|
130
|
+
'the registered public subagents (see the `spawn_subagent` tool description for the live list and each one’s purpose)'
|
|
131
|
+
|
|
132
|
+
// Back-compat constant: the full prompt with the placeholder roster baked in.
|
|
133
|
+
// Retained because several tests assert `prompt.startsWith(DEFAULT_SYSTEM_PROMPT)`
|
|
134
|
+
// on the no-registry path; production full-mode composition substitutes the real
|
|
135
|
+
// roster via `buildDefaultSystemPrompt`.
|
|
136
|
+
export const DEFAULT_SYSTEM_PROMPT = buildDefaultSystemPrompt(DEFAULT_SUBAGENT_ROSTER)
|
|
112
137
|
|
|
113
138
|
// Stable, low-volatility metadata about the runtime hosting the agent.
|
|
114
139
|
// Rendered into the system prompt just below DEFAULT_SYSTEM_PROMPT + identity
|