indusagi-coding-agent 0.2.8 → 0.2.9
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 +2 -1
- package/src/_decls/entry.ts +18 -0
- package/src/_decls/guardrails.ts +35 -0
- package/src/_decls/index.ts +26 -0
- package/src/addons/contract.ts +236 -0
- package/src/addons/dispatch/event-dispatcher.ts +164 -0
- package/src/addons/dispatch/index.ts +25 -0
- package/src/addons/dispatch/tool-interceptor.ts +208 -0
- package/src/addons/host.ts +225 -0
- package/src/addons/index.ts +112 -0
- package/src/addons/manifest.ts +158 -0
- package/src/addons/sandbox.ts +170 -0
- package/src/addons/surface.ts +78 -0
- package/src/boot/auth-vault.ts +195 -0
- package/src/boot/boot.ts +138 -0
- package/src/boot/contract.ts +238 -0
- package/src/boot/heap.ts +59 -0
- package/src/boot/index.ts +28 -0
- package/src/boot/invocation.ts +93 -0
- package/src/boot/runners/addon-wiring.ts +153 -0
- package/src/boot/runners/checkpoint.ts +169 -0
- package/src/boot/runners/delegate-runner.ts +294 -0
- package/src/boot/runners/index.ts +13 -0
- package/src/boot/runners/link-runner.ts +45 -0
- package/src/boot/runners/memdir.ts +168 -0
- package/src/boot/runners/oneshot-runner.ts +58 -0
- package/src/boot/runners/read-state.ts +90 -0
- package/src/boot/runners/registry.ts +42 -0
- package/src/boot/runners/repl-runner.ts +143 -0
- package/src/boot/runners/server-mode.ts +121 -0
- package/src/boot/runners/session.ts +641 -0
- package/src/boot/server-token.ts +148 -0
- package/src/boot/stages.ts +167 -0
- package/src/boot/upgrade/apply.ts +94 -0
- package/src/boot/upgrade/index.ts +13 -0
- package/src/boot/upgrade/upgrades.ts +289 -0
- package/src/briefing/compose.ts +150 -0
- package/src/briefing/context-docs.ts +19 -0
- package/src/briefing/contract.ts +717 -0
- package/src/briefing/index.ts +31 -0
- package/src/briefing/macros.ts +97 -0
- package/src/briefing/skills.ts +47 -0
- package/src/capability-deck/bridge-ledger/index.ts +27 -0
- package/src/capability-deck/bridge-ledger/key.ts +67 -0
- package/src/capability-deck/bridge-ledger/ledger.ts +131 -0
- package/src/capability-deck/bridge-ledger/network.ts +117 -0
- package/src/capability-deck/builtin-bridge.ts +312 -0
- package/src/capability-deck/cards/bg-process-card.ts +335 -0
- package/src/capability-deck/cards/index.ts +115 -0
- package/src/capability-deck/cards/memory-card.ts +146 -0
- package/src/capability-deck/cards/plan-file.ts +97 -0
- package/src/capability-deck/cards/plan-tools.ts +185 -0
- package/src/capability-deck/cards/saas-card.ts +183 -0
- package/src/capability-deck/cards/task-card.ts +207 -0
- package/src/capability-deck/cards/todo-card.ts +168 -0
- package/src/capability-deck/cards/workflow-card.ts +247 -0
- package/src/capability-deck/contract.ts +388 -0
- package/src/capability-deck/index.ts +48 -0
- package/src/capability-deck/manifest.ts +109 -0
- package/src/capability-deck/provision.ts +169 -0
- package/src/channels/contract.ts +191 -0
- package/src/channels/framer.ts +50 -0
- package/src/channels/index.ts +101 -0
- package/src/channels/link/dialog.ts +129 -0
- package/src/channels/link/driver.ts +190 -0
- package/src/channels/link/index.ts +34 -0
- package/src/channels/link/server.ts +134 -0
- package/src/channels/oneshot.ts +90 -0
- package/src/channels/ops.ts +65 -0
- package/src/channels/session-ops.ts +81 -0
- package/src/conductor/bash-guard.ts +599 -0
- package/src/conductor/catalog/catalog.ts +116 -0
- package/src/conductor/catalog/index.ts +8 -0
- package/src/conductor/catalog/matcher.ts +134 -0
- package/src/conductor/conductor.ts +234 -0
- package/src/conductor/contract.ts +842 -0
- package/src/conductor/diagnostics.ts +227 -0
- package/src/conductor/index.ts +33 -0
- package/src/conductor/permissions.ts +588 -0
- package/src/conductor/quota-error.ts +49 -0
- package/src/conductor/signal-hub/hub.ts +46 -0
- package/src/conductor/signal-hub/index.ts +2 -0
- package/src/conductor/signal-hub/translate.test.ts +81 -0
- package/src/conductor/signal-hub/translate.ts +74 -0
- package/src/conductor/skill-parse/index.ts +2 -0
- package/src/conductor/skill-parse/parse.ts +108 -0
- package/src/conductor/transcript-store/index.ts +22 -0
- package/src/conductor/transcript-store/serialize.ts +116 -0
- package/src/conductor/transcript-store/store.ts +205 -0
- package/src/console/auth-status.ts +56 -0
- package/src/console/components/AgentsView.ts +165 -0
- package/src/console/components/BackgroundAgents.ts +155 -0
- package/src/console/components/Banner.ts +334 -0
- package/src/console/components/Composer.ts +94 -0
- package/src/console/components/StatusBar.ts +49 -0
- package/src/console/components/TerminalConsole.ts +1090 -0
- package/src/console/components/WorkingIndicator.ts +98 -0
- package/src/console/components/banner-sweep.ts +24 -0
- package/src/console/components/welcome.ts +74 -0
- package/src/console/contract.ts +630 -0
- package/src/console/index.ts +34 -0
- package/src/console/input/complete.ts +127 -0
- package/src/console/input/dir-reader.ts +34 -0
- package/src/console/input/index.ts +23 -0
- package/src/console/input/keymap.ts +159 -0
- package/src/console/input/paste.ts +104 -0
- package/src/console/mount.ts +56 -0
- package/src/console/overlays/approval-queue.ts +57 -0
- package/src/console/overlays/approval.ts +130 -0
- package/src/console/overlays/auth.ts +342 -0
- package/src/console/overlays/boards.ts +308 -0
- package/src/console/overlays/host.ts +36 -0
- package/src/console/overlays/index.ts +26 -0
- package/src/console/overlays/pickers.ts +258 -0
- package/src/console/overlays/sessions.ts +190 -0
- package/src/console/reducer.ts +182 -0
- package/src/console/slash/builtins.ts +81 -0
- package/src/console/slash/commands/dynamic.ts +83 -0
- package/src/console/slash/commands/integrations.ts +695 -0
- package/src/console/slash/commands/shared.ts +75 -0
- package/src/console/slash/commands/transcript.ts +263 -0
- package/src/console/slash/commands/workbench.ts +246 -0
- package/src/console/slash/index.ts +15 -0
- package/src/console/slash/registry.ts +70 -0
- package/src/console/slash/resolve.ts +63 -0
- package/src/console/startup.ts +209 -0
- package/src/console/theme/adapter.ts +45 -0
- package/src/console/theme/index.ts +7 -0
- package/src/console/theme/palette.ts +68 -0
- package/src/console/theme/resolve.ts +39 -0
- package/src/console/theme/tokens.ts +71 -0
- package/src/entry.ts +55 -0
- package/src/guardrails.ts +37 -0
- package/src/index.ts +18 -0
- package/src/insight/channel.ts +88 -0
- package/src/insight/contract.ts +185 -0
- package/src/insight/index.ts +110 -0
- package/src/insight/recorder.ts +213 -0
- package/src/insight/redaction.ts +157 -0
- package/src/insight/replay.ts +158 -0
- package/src/insight/sampling.ts +70 -0
- package/src/insight/serialize.ts +50 -0
- package/src/insight/sinks/console.ts +64 -0
- package/src/insight/sinks/file.ts +40 -0
- package/src/insight/sinks/index.ts +24 -0
- package/src/insight/sinks/stream.ts +54 -0
- package/src/integrations/sarvam/attach.ts +239 -0
- package/src/integrations/sarvam/config.ts +156 -0
- package/src/integrations/sarvam/index.ts +25 -0
- package/src/integrations/sarvam/sarvam.test.ts +60 -0
- package/src/integrations/sarvam/types.ts +27 -0
- package/src/integrations/zoho/attach.ts +342 -0
- package/src/integrations/zoho/config.ts +125 -0
- package/src/integrations/zoho/index.ts +27 -0
- package/src/integrations/zoho/types.ts +21 -0
- package/src/integrations/zoho/zoho.test.ts +50 -0
- package/src/kit/clipboard-image.ts +107 -0
- package/src/kit/external-editor.ts +48 -0
- package/src/kit/image.ts +59 -0
- package/src/kit/index.ts +51 -0
- package/src/kit/shell.ts +19 -0
- package/src/kit/tool-fetch.ts +85 -0
- package/src/launch/catalog.ts +148 -0
- package/src/launch/contract.ts +187 -0
- package/src/launch/credentials.ts +625 -0
- package/src/launch/index.ts +98 -0
- package/src/launch/invocation/attachments.ts +179 -0
- package/src/launch/invocation/flags.ts +196 -0
- package/src/launch/invocation/index.ts +25 -0
- package/src/launch/invocation/read.ts +260 -0
- package/src/launch/invocation/usage.ts +67 -0
- package/src/launch/login.ts +324 -0
- package/src/launch/oauth.test.ts +18 -0
- package/src/launch/oauth.ts +203 -0
- package/src/launch/packages.ts +194 -0
- package/src/launch/pickers.ts +189 -0
- package/src/runtime-bridge/bridges/_drive.ts +96 -0
- package/src/runtime-bridge/bridges/builtins.ts +68 -0
- package/src/runtime-bridge/bridges/claude-cli.ts +123 -0
- package/src/runtime-bridge/bridges/codex-cli.ts +142 -0
- package/src/runtime-bridge/bridges/index.ts +33 -0
- package/src/runtime-bridge/bridges/indusagi-cli.ts +155 -0
- package/src/runtime-bridge/broker.ts +227 -0
- package/src/runtime-bridge/contract.ts +122 -0
- package/src/runtime-bridge/index.ts +79 -0
- package/src/runtime-bridge/sink.ts +180 -0
- package/src/sessions/contract.ts +81 -0
- package/src/sessions/index.ts +13 -0
- package/src/sessions/library.ts +229 -0
- package/src/settings/contract.ts +114 -0
- package/src/settings/index.ts +32 -0
- package/src/settings/manager.ts +117 -0
- package/src/transcript-export/index.ts +45 -0
- package/src/transcript-export/publish.ts +260 -0
- package/src/transcript-export/sgr.ts +315 -0
- package/src/transcript-export/template.ts +272 -0
- package/src/transcript-export/theme-bridge.ts +150 -0
- package/src/window-budget/budget/estimate.ts +135 -0
- package/src/window-budget/budget/gate.ts +33 -0
- package/src/window-budget/budget/index.ts +16 -0
- package/src/window-budget/budget/slice.ts +56 -0
- package/src/window-budget/condenser.ts +58 -0
- package/src/window-budget/contract.ts +184 -0
- package/src/window-budget/index.ts +19 -0
- package/src/window-budget/microcompact.ts +95 -0
- package/src/window-budget/rehydrate.ts +136 -0
- package/src/window-budget/summarize/condense.ts +103 -0
- package/src/window-budget/summarize/index.ts +14 -0
- package/src/window-budget/summarize/prompt.ts +149 -0
- package/src/workflow-engine/agent-runner.ts +181 -0
- package/src/workflow-engine/display.ts +224 -0
- package/src/workflow-engine/engine.ts +294 -0
- package/src/workflow-engine/index.ts +23 -0
- package/src/workflow-engine/parse.ts +172 -0
- package/src/workflow-engine/structured-output.ts +35 -0
- package/src/workspace/brand.ts +29 -0
- package/src/workspace/index.ts +18 -0
- package/src/workspace/locator.ts +103 -0
- package/src/workspace/runtime-detect.ts +64 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in capability bridge — ONE seam to the framework's native tools.
|
|
3
|
+
*
|
|
4
|
+
* The framework (`indusagi/agent`, the `actions` surface) already authors its
|
|
5
|
+
* file, search, shell, web, process, and checklist tools as fully-formed
|
|
6
|
+
* `AgentTool` objects and ships a `create<Name>Tool(cwd, options)` factory for
|
|
7
|
+
* each. This module is the single place the deck reaches across that seam: it
|
|
8
|
+
* pairs every native factory with a deck {@link CapabilityCard.build} closure so
|
|
9
|
+
* the whole framework tool set is re-exposed as {@link Capability} rows in one
|
|
10
|
+
* file rather than in a dozen one-line re-export stubs.
|
|
11
|
+
*
|
|
12
|
+
* What this buys the rest of the deck:
|
|
13
|
+
* - {@link BUILTIN_BRIDGE} — a single record keyed by the wire-facing tool
|
|
14
|
+
* name, each value a {@link BridgeBuilder} that mints the live capability for
|
|
15
|
+
* a {@link DeckContext}. The manifest derives its catalog rows from this
|
|
16
|
+
* record; nothing else hand-maintains a parallel list of built-ins.
|
|
17
|
+
* - {@link buildBuiltin} — resolve-and-build one built-in by id, surfacing a
|
|
18
|
+
* typed {@link DeckFault} on an unknown id or a builder throw.
|
|
19
|
+
* - {@link BUILTIN_PROFILES} — the per-built-in profile membership the
|
|
20
|
+
* data-driven provisioner intersects with a requested {@link DeckProfile},
|
|
21
|
+
* so the survey (observe-only) set falls out of one table instead of a
|
|
22
|
+
* second hand-written tool list.
|
|
23
|
+
*
|
|
24
|
+
* Design notes:
|
|
25
|
+
* - The builders thread `ctx.cwd` (and, where a factory takes them, the
|
|
26
|
+
* framework option bags) into the native factory. They never reimplement the
|
|
27
|
+
* tool — the framework owns the read/edit/grep/bash/etc. behavior; the bridge
|
|
28
|
+
* only *binds* it to a working context and re-labels it for the catalog.
|
|
29
|
+
* - The process and checklist tools are stateful framework singletons exposed
|
|
30
|
+
* through their factories; the bridge keeps them framework-backed (wrapping
|
|
31
|
+
* the framework, not any third-party package) while presenting them as plain
|
|
32
|
+
* capabilities to the deck.
|
|
33
|
+
* - All model-facing tool `name` strings are the framework's own wire contract
|
|
34
|
+
* (`read`, `bash`, `grep`, …) and are kept verbatim — the model's prompt and
|
|
35
|
+
* the conductor key off them. Only the deck-side titles/summaries are the
|
|
36
|
+
* deck's own prose.
|
|
37
|
+
*/
|
|
38
|
+
import {
|
|
39
|
+
createReadTool,
|
|
40
|
+
createWriteTool,
|
|
41
|
+
createEditTool,
|
|
42
|
+
createLsTool,
|
|
43
|
+
createGrepTool,
|
|
44
|
+
createFindTool,
|
|
45
|
+
createBashTool,
|
|
46
|
+
createProcessTool,
|
|
47
|
+
createWebSearchTool,
|
|
48
|
+
createWebFetchTool,
|
|
49
|
+
createTodoReadTool,
|
|
50
|
+
createTodoWriteTool,
|
|
51
|
+
} from "indusagi/agent";
|
|
52
|
+
import {
|
|
53
|
+
capabilityId,
|
|
54
|
+
deckFault,
|
|
55
|
+
type AnyCapability,
|
|
56
|
+
type Capability,
|
|
57
|
+
type CapabilityId,
|
|
58
|
+
type CardProfiles,
|
|
59
|
+
type DeckContext,
|
|
60
|
+
} from "./contract.js";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A closure that mints one framework built-in {@link Capability} for a working
|
|
64
|
+
* context. The deck's view of a native factory after the bridge has bound the
|
|
65
|
+
* `cwd` (and any option bag) the factory needs.
|
|
66
|
+
*
|
|
67
|
+
* Returns `AnyCapability` because the heterogeneous built-ins carry different
|
|
68
|
+
* parameter schemas; a typed `AgentTool<TSchema>` widens to this element type,
|
|
69
|
+
* which is exactly what the deck and the conductor consume.
|
|
70
|
+
*/
|
|
71
|
+
export type BridgeBuilder = (ctx: DeckContext) => AnyCapability;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* One descriptor in the built-in catalog: the wire-facing id, the deck-side
|
|
75
|
+
* title/summary prose, the profiles the tool participates in, and the
|
|
76
|
+
* {@link BridgeBuilder} that binds the framework factory to a context.
|
|
77
|
+
*
|
|
78
|
+
* The manifest turns each of these into a {@link CapabilityCard}; consumers that
|
|
79
|
+
* want the raw built-in set read {@link BUILTIN_BRIDGE} directly.
|
|
80
|
+
*/
|
|
81
|
+
export interface BuiltinDescriptor {
|
|
82
|
+
/** Wire-facing tool name the model invokes (framework contract, verbatim). */
|
|
83
|
+
readonly id: CapabilityId;
|
|
84
|
+
/** Short human-facing title for catalogs and help text. */
|
|
85
|
+
readonly title: string;
|
|
86
|
+
/** One-line description in the deck's own voice. */
|
|
87
|
+
readonly summary: string;
|
|
88
|
+
/** Which deck profiles this built-in belongs to. */
|
|
89
|
+
readonly profiles: CardProfiles;
|
|
90
|
+
/** Bind the framework factory to a working context. */
|
|
91
|
+
readonly build: BridgeBuilder;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Profile membership for read-only framework built-ins.
|
|
96
|
+
*
|
|
97
|
+
* Every observe-only built-in (read/ls/grep/find/web search & fetch/checklist
|
|
98
|
+
* read) participates in both `authoring` (sub-agent) and `survey` (full
|
|
99
|
+
* read-only) profiles.
|
|
100
|
+
*/
|
|
101
|
+
export const READ_ONLY: CardProfiles = ["authoring", "survey"];
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Profile membership for mutating framework built-ins.
|
|
105
|
+
*
|
|
106
|
+
* Mutating built-ins (write/edit/bash/process/checklist write) participate only
|
|
107
|
+
* in the `authoring` profile (sub-agents), not the read-only survey set.
|
|
108
|
+
*/
|
|
109
|
+
export const MUTATING: CardProfiles = ["authoring"];
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The ordered list of built-in descriptors — the manifest projects its catalog
|
|
113
|
+
* rows from this, preserving order.
|
|
114
|
+
*
|
|
115
|
+
* Each row pairs a framework-native factory (`create<Name>Tool`) with deck-side
|
|
116
|
+
* prose and profile membership. Order is the model's enumeration order.
|
|
117
|
+
*/
|
|
118
|
+
export const BUILTIN_DESCRIPTORS: readonly BuiltinDescriptor[] = [
|
|
119
|
+
{
|
|
120
|
+
id: capabilityId("read"),
|
|
121
|
+
title: "Read file",
|
|
122
|
+
summary:
|
|
123
|
+
"Return the contents of a file at a path, optionally from an offset and capped to a line limit; renders images inline where supported.",
|
|
124
|
+
profiles: READ_ONLY,
|
|
125
|
+
build: (ctx) =>
|
|
126
|
+
createReadTool(ctx.cwd, {
|
|
127
|
+
readState: ctx.framework?.["readState"],
|
|
128
|
+
}),
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: capabilityId("ls"),
|
|
132
|
+
title: "List directory",
|
|
133
|
+
summary: "Enumerate the entries of a directory, with an optional cap on how many are returned.",
|
|
134
|
+
profiles: READ_ONLY,
|
|
135
|
+
build: (ctx) => createLsTool(ctx.cwd),
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: capabilityId("grep"),
|
|
139
|
+
title: "Search file contents",
|
|
140
|
+
summary:
|
|
141
|
+
"Scan files for lines matching a pattern, with optional case-insensitivity, literal matching, surrounding context, and a result cap.",
|
|
142
|
+
profiles: READ_ONLY,
|
|
143
|
+
build: (ctx) => createGrepTool(ctx.cwd),
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: capabilityId("find"),
|
|
147
|
+
title: "Find files by name",
|
|
148
|
+
summary:
|
|
149
|
+
"Locate files and directories whose names match a glob-style pattern beneath a root, capped to a result limit.",
|
|
150
|
+
profiles: READ_ONLY,
|
|
151
|
+
build: (ctx) => createFindTool(ctx.cwd),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: capabilityId("websearch"),
|
|
155
|
+
title: "Web search",
|
|
156
|
+
summary:
|
|
157
|
+
"Query the live web for a string and return a ranked set of result snippets, capped to a requested count.",
|
|
158
|
+
profiles: READ_ONLY,
|
|
159
|
+
build: () => createWebSearchTool(),
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: capabilityId("webfetch"),
|
|
163
|
+
title: "Fetch URL",
|
|
164
|
+
summary:
|
|
165
|
+
"Retrieve a single URL and return its body as text, Markdown, or HTML, with an optional request timeout.",
|
|
166
|
+
profiles: READ_ONLY,
|
|
167
|
+
build: () => createWebFetchTool(),
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: capabilityId("todoread"),
|
|
171
|
+
title: "Read checklist",
|
|
172
|
+
summary:
|
|
173
|
+
"Read back the session's running task checklist so the agent can review what is pending, active, or done.",
|
|
174
|
+
profiles: READ_ONLY,
|
|
175
|
+
// The framework's default in-memory checklist singleton; a session-aware
|
|
176
|
+
// persistent store is wired by the separate checklist provisioner, not here.
|
|
177
|
+
build: () => createTodoReadTool(),
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: capabilityId("write"),
|
|
181
|
+
title: "Write file",
|
|
182
|
+
summary: "Create or overwrite a file at a path with the given contents, making parent directories as needed.",
|
|
183
|
+
profiles: MUTATING,
|
|
184
|
+
build: (ctx) =>
|
|
185
|
+
createWriteTool(ctx.cwd, {
|
|
186
|
+
readState: ctx.framework?.["readState"],
|
|
187
|
+
checkpoint: ctx.framework?.["checkpoint"],
|
|
188
|
+
}),
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: capabilityId("edit"),
|
|
192
|
+
title: "Edit file",
|
|
193
|
+
summary:
|
|
194
|
+
"Apply a find-and-replace edit to a file, swapping an exact span of old text for new text and reporting the resulting diff.",
|
|
195
|
+
profiles: MUTATING,
|
|
196
|
+
build: (ctx) =>
|
|
197
|
+
createEditTool(ctx.cwd, {
|
|
198
|
+
readState: ctx.framework?.["readState"],
|
|
199
|
+
checkpoint: ctx.framework?.["checkpoint"],
|
|
200
|
+
}),
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: capabilityId("bash"),
|
|
204
|
+
title: "Run shell command",
|
|
205
|
+
summary:
|
|
206
|
+
"Execute a shell command in the working directory, streaming its output and honoring an optional timeout.",
|
|
207
|
+
profiles: MUTATING,
|
|
208
|
+
build: (ctx) => createBashTool(ctx.cwd),
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: capabilityId("process"),
|
|
212
|
+
title: "Manage background processes",
|
|
213
|
+
summary:
|
|
214
|
+
"Start, list, inspect, feed input to, and stop long-running background commands without blocking the agent on them.",
|
|
215
|
+
profiles: MUTATING,
|
|
216
|
+
build: (ctx) => createProcessTool({ cwd: ctx.cwd }),
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: capabilityId("todowrite"),
|
|
220
|
+
title: "Update checklist",
|
|
221
|
+
summary: "Replace or amend the session's task checklist, setting each item's text, status, and priority.",
|
|
222
|
+
profiles: MUTATING,
|
|
223
|
+
// Paired with the read singleton above; both share the framework default
|
|
224
|
+
// in-memory store. The checklist provisioner swaps in a persistent store.
|
|
225
|
+
build: () => createTodoWriteTool(),
|
|
226
|
+
},
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The framework built-ins keyed by their wire-facing {@link CapabilityId} — the
|
|
231
|
+
* single record the manifest and provisioner read from.
|
|
232
|
+
*
|
|
233
|
+
* Derived from {@link BUILTIN_DESCRIPTORS}, so the keyed view and the ordered
|
|
234
|
+
* list never drift: there is exactly one place a built-in is declared.
|
|
235
|
+
*/
|
|
236
|
+
export const BUILTIN_BRIDGE: Readonly<Record<string, BuiltinDescriptor>> = Object.freeze(
|
|
237
|
+
Object.fromEntries(BUILTIN_DESCRIPTORS.map((d) => [d.id, d])),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The wire-facing ids of every framework built-in, in catalog order. Convenient
|
|
242
|
+
* for `--tools` style selection and for the manifest's projection.
|
|
243
|
+
*/
|
|
244
|
+
export const BUILTIN_IDS: readonly CapabilityId[] = BUILTIN_DESCRIPTORS.map((d) => d.id);
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The profile membership of every built-in, keyed by id — the slice of the
|
|
248
|
+
* profile table the data-driven provisioner reads to decide which built-ins a
|
|
249
|
+
* requested {@link DeckProfile} includes.
|
|
250
|
+
*/
|
|
251
|
+
export const BUILTIN_PROFILES: Readonly<Record<string, CardProfiles>> = Object.freeze(
|
|
252
|
+
Object.fromEntries(BUILTIN_DESCRIPTORS.map((d) => [d.id, d.profiles])),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The ordered list of built-in descriptors — the manifest projects its catalog
|
|
257
|
+
* rows from this, preserving order.
|
|
258
|
+
*/
|
|
259
|
+
export function builtinDescriptors(): readonly BuiltinDescriptor[] {
|
|
260
|
+
return BUILTIN_DESCRIPTORS;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Resolve a built-in by id and build its live capability for a context.
|
|
265
|
+
*
|
|
266
|
+
* Throws a typed {@link DeckFault}: `unknown_capability` when the id is not a
|
|
267
|
+
* framework built-in, or `build_failed` when the framework factory throws. This
|
|
268
|
+
* is the sanctioned entry point so failure shaping stays uniform across the deck.
|
|
269
|
+
*
|
|
270
|
+
* @param id the wire-facing built-in name to build
|
|
271
|
+
* @param ctx the working context to bind the factory to
|
|
272
|
+
*/
|
|
273
|
+
export function buildBuiltin(id: CapabilityId, ctx: DeckContext): Capability {
|
|
274
|
+
const descriptor = BUILTIN_BRIDGE[id];
|
|
275
|
+
if (descriptor === undefined) {
|
|
276
|
+
throw deckFault(
|
|
277
|
+
"unknown_capability",
|
|
278
|
+
`No framework built-in is registered under "${id}".`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
return descriptor.build(ctx);
|
|
283
|
+
} catch (cause) {
|
|
284
|
+
throw deckFault(
|
|
285
|
+
"build_failed",
|
|
286
|
+
`Framework built-in "${id}" failed to build.`,
|
|
287
|
+
cause,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Build every framework built-in eligible for a profile, bound to a context.
|
|
294
|
+
*
|
|
295
|
+
* Walks the bridge table once, keeps each built-in whose profile membership
|
|
296
|
+
* admits the requested profile (`all` admits everything), and binds it. A single
|
|
297
|
+
* data-driven pass replaces a trio of per-profile build functions.
|
|
298
|
+
*
|
|
299
|
+
* @param profile the requested deck profile
|
|
300
|
+
* @param ctx the working context to bind each factory to
|
|
301
|
+
*/
|
|
302
|
+
export function buildBuiltinsForProfile(
|
|
303
|
+
profile: "authoring" | "survey" | "all",
|
|
304
|
+
ctx: DeckContext,
|
|
305
|
+
): AnyCapability[] {
|
|
306
|
+
const built: AnyCapability[] = [];
|
|
307
|
+
for (const descriptor of BUILTIN_DESCRIPTORS) {
|
|
308
|
+
if (profile !== "all" && !descriptor.profiles.includes(profile)) continue;
|
|
309
|
+
built.push(buildBuiltin(descriptor.id, ctx));
|
|
310
|
+
}
|
|
311
|
+
return built;
|
|
312
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background-process capability — start, poll, and stop long-lived child
|
|
3
|
+
* processes that outlive a single tool call.
|
|
4
|
+
*
|
|
5
|
+
* App-novel and framework-agnostic: it wraps Node's `child_process.spawn`
|
|
6
|
+
* directly rather than delegating to any framework process controller, so the
|
|
7
|
+
* lifecycle is owned entirely by this card. A long-running command (a dev
|
|
8
|
+
* server, a watcher, a build in `--watch` mode) is launched detached from the
|
|
9
|
+
* tool turn; the agent later polls its captured output or signals it to stop.
|
|
10
|
+
*
|
|
11
|
+
* One tool folds the lifecycle into an `action` discriminant:
|
|
12
|
+
* - `start` — spawn `command` in a shell, return the assigned handle id.
|
|
13
|
+
* - `poll` — return the buffered stdout/stderr (and live/exited status) for a
|
|
14
|
+
* handle, optionally only the lines appended since the last poll.
|
|
15
|
+
* - `stop` — send SIGTERM (escalating to SIGKILL after a grace window) to a
|
|
16
|
+
* handle's process tree.
|
|
17
|
+
* - `list` — enumerate every handle this capability is tracking.
|
|
18
|
+
*
|
|
19
|
+
* Output is captured into bounded ring buffers so a chatty process cannot grow
|
|
20
|
+
* memory without limit; only the most recent lines are retained. The card
|
|
21
|
+
* produces a {@link Capability} (framework `AgentTool`) the conductor consumes.
|
|
22
|
+
*/
|
|
23
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
24
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
25
|
+
import { capabilityId, type Capability, type CapabilityCard, type DeckContext } from "../contract.js";
|
|
26
|
+
|
|
27
|
+
/** Coarse lifecycle state of a tracked background process. */
|
|
28
|
+
export type DaemonState = "running" | "exited" | "signalled";
|
|
29
|
+
|
|
30
|
+
/** A bounded buffer of recent output lines for one stream. */
|
|
31
|
+
interface RingBuffer {
|
|
32
|
+
lines: string[];
|
|
33
|
+
/** Index of the next unread line for incremental polling. */
|
|
34
|
+
cursor: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** One tracked background process and its captured state. */
|
|
38
|
+
interface DaemonHandle {
|
|
39
|
+
readonly id: string;
|
|
40
|
+
readonly command: string;
|
|
41
|
+
readonly child: ChildProcess;
|
|
42
|
+
state: DaemonState;
|
|
43
|
+
exitCode: number | null;
|
|
44
|
+
signal: NodeJS.Signals | null;
|
|
45
|
+
readonly startedAt: number;
|
|
46
|
+
readonly stdout: RingBuffer;
|
|
47
|
+
readonly stderr: RingBuffer;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Cap on how many output lines one ring buffer keeps per stream. */
|
|
51
|
+
const MAX_BUFFERED_LINES = 2_000;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* An in-process table of background children, owned by one built capability.
|
|
55
|
+
*
|
|
56
|
+
* Each `start` spawns a shell-wrapped child, wires its stdout/stderr into
|
|
57
|
+
* bounded buffers, and records terminal state on `exit`. `stop` escalates from
|
|
58
|
+
* SIGTERM to SIGKILL if the process does not exit within a grace window. The
|
|
59
|
+
* table is per-capability, so one session's daemons are isolated from another's.
|
|
60
|
+
*/
|
|
61
|
+
export class DaemonTable {
|
|
62
|
+
private readonly handles = new Map<string, DaemonHandle>();
|
|
63
|
+
private nextSeq = 1;
|
|
64
|
+
private readonly cwd: string;
|
|
65
|
+
|
|
66
|
+
constructor(cwd: string) {
|
|
67
|
+
this.cwd = cwd;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
start(command: string): DaemonHandle {
|
|
71
|
+
const id = `bg-${this.nextSeq++}`;
|
|
72
|
+
const child = spawn(command, {
|
|
73
|
+
cwd: this.cwd,
|
|
74
|
+
shell: true,
|
|
75
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
76
|
+
detached: false,
|
|
77
|
+
});
|
|
78
|
+
const handle: DaemonHandle = {
|
|
79
|
+
id,
|
|
80
|
+
command,
|
|
81
|
+
child,
|
|
82
|
+
state: "running",
|
|
83
|
+
exitCode: null,
|
|
84
|
+
signal: null,
|
|
85
|
+
startedAt: Date.now(),
|
|
86
|
+
stdout: { lines: [], cursor: 0 },
|
|
87
|
+
stderr: { lines: [], cursor: 0 },
|
|
88
|
+
};
|
|
89
|
+
child.stdout?.setEncoding("utf8");
|
|
90
|
+
child.stderr?.setEncoding("utf8");
|
|
91
|
+
child.stdout?.on("data", (d) => pushLines(handle.stdout, d));
|
|
92
|
+
child.stderr?.on("data", (d) => pushLines(handle.stderr, d));
|
|
93
|
+
child.on("exit", (code, sig) => {
|
|
94
|
+
handle.exitCode = code;
|
|
95
|
+
handle.signal = sig;
|
|
96
|
+
handle.state = sig ? "signalled" : "exited";
|
|
97
|
+
});
|
|
98
|
+
child.on("error", (err) => {
|
|
99
|
+
pushLines(handle.stderr, `spawn error: ${String(err)}`);
|
|
100
|
+
handle.state = "exited";
|
|
101
|
+
});
|
|
102
|
+
this.handles.set(id, handle);
|
|
103
|
+
return handle;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get(id: string): DaemonHandle | undefined {
|
|
107
|
+
return this.handles.get(id);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
list(): DaemonHandle[] {
|
|
111
|
+
return [...this.handles.values()];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Signal a handle's process to terminate. Sends SIGTERM immediately and a
|
|
116
|
+
* SIGKILL after `graceMs` if the child is still alive. Resolves once the
|
|
117
|
+
* child has exited or the kill has been sent.
|
|
118
|
+
*/
|
|
119
|
+
async stop(handle: DaemonHandle, graceMs = 3_000): Promise<void> {
|
|
120
|
+
if (handle.state !== "running") return;
|
|
121
|
+
const child = handle.child;
|
|
122
|
+
child.kill("SIGTERM");
|
|
123
|
+
await new Promise<void>((resolve) => {
|
|
124
|
+
const timer = setTimeout(() => {
|
|
125
|
+
if (handle.state === "running") child.kill("SIGKILL");
|
|
126
|
+
resolve();
|
|
127
|
+
}, graceMs);
|
|
128
|
+
child.once("exit", () => {
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
resolve();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Append chunked stream output to a ring buffer, trimming the oldest lines when full. */
|
|
137
|
+
function pushLines(buf: RingBuffer, chunk: string): void {
|
|
138
|
+
for (const line of chunk.split(/\r?\n/)) {
|
|
139
|
+
if (line.length === 0) continue;
|
|
140
|
+
buf.lines.push(line);
|
|
141
|
+
}
|
|
142
|
+
if (buf.lines.length > MAX_BUFFERED_LINES) {
|
|
143
|
+
const drop = buf.lines.length - MAX_BUFFERED_LINES;
|
|
144
|
+
buf.lines.splice(0, drop);
|
|
145
|
+
buf.cursor = Math.max(0, buf.cursor - drop);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Drain the buffered output, optionally only since the last read. */
|
|
150
|
+
function drainBuffer(buf: RingBuffer, sinceLast: boolean): string[] {
|
|
151
|
+
if (sinceLast) {
|
|
152
|
+
const fresh = buf.lines.slice(buf.cursor);
|
|
153
|
+
buf.cursor = buf.lines.length;
|
|
154
|
+
return fresh;
|
|
155
|
+
}
|
|
156
|
+
return [...buf.lines];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Format a handle's status for model-facing prose. */
|
|
160
|
+
function statusLine(h: DaemonHandle): string {
|
|
161
|
+
if (h.state === "running") return "running";
|
|
162
|
+
if (h.state === "signalled") return `signalled (${h.signal ?? "?"})`;
|
|
163
|
+
return `exited (code ${h.exitCode ?? "?"})`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const DaemonParams = Type.Object(
|
|
167
|
+
{
|
|
168
|
+
action: Type.Union(
|
|
169
|
+
[
|
|
170
|
+
Type.Literal("start"),
|
|
171
|
+
Type.Literal("poll"),
|
|
172
|
+
Type.Literal("stop"),
|
|
173
|
+
Type.Literal("list"),
|
|
174
|
+
],
|
|
175
|
+
{
|
|
176
|
+
description:
|
|
177
|
+
"`start` launches a long-running command; `poll` reads its captured output; `stop` terminates it; `list` shows all tracked processes.",
|
|
178
|
+
},
|
|
179
|
+
),
|
|
180
|
+
command: Type.Optional(
|
|
181
|
+
Type.String({
|
|
182
|
+
description: "Shell command to launch. Required for `start`.",
|
|
183
|
+
}),
|
|
184
|
+
),
|
|
185
|
+
id: Type.Optional(
|
|
186
|
+
Type.String({
|
|
187
|
+
description: "Handle returned by `start`. Required for `poll` and `stop`.",
|
|
188
|
+
}),
|
|
189
|
+
),
|
|
190
|
+
sinceLast: Type.Optional(
|
|
191
|
+
Type.Boolean({
|
|
192
|
+
description:
|
|
193
|
+
"On `poll`, return only output appended since the previous poll of this handle. Defaults to false (return the full retained buffer).",
|
|
194
|
+
}),
|
|
195
|
+
),
|
|
196
|
+
},
|
|
197
|
+
{ additionalProperties: false },
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
/** Statically-inferred parameter type the capability's `execute` receives. */
|
|
201
|
+
export type DaemonParamsType = Static<typeof DaemonParams>;
|
|
202
|
+
|
|
203
|
+
/** Structured detail returned alongside the model-facing content. */
|
|
204
|
+
export interface DaemonDetails {
|
|
205
|
+
readonly action: "start" | "poll" | "stop" | "list";
|
|
206
|
+
readonly ok: boolean;
|
|
207
|
+
readonly id?: string;
|
|
208
|
+
readonly state?: DaemonState;
|
|
209
|
+
readonly exitCode?: number | null;
|
|
210
|
+
readonly processes?: ReadonlyArray<{
|
|
211
|
+
readonly id: string;
|
|
212
|
+
readonly command: string;
|
|
213
|
+
readonly state: DaemonState;
|
|
214
|
+
}>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const DAEMON_DESCRIPTION =
|
|
218
|
+
'Run and manage long-lived background processes that persist across tool calls \u2014 dev servers, file watchers, anything you start once and observe over time. Use `action:"start"` with a `command` to launch one (you get back a handle `id`), `action:"poll"` with that `id` to read its accumulated output, `action:"stop"` to terminate it, and `action:"list"` to see what is running. Do NOT use this for ordinary one-shot commands that finish on their own \u2014 run those with the shell tool.';
|
|
219
|
+
|
|
220
|
+
/** Standard error result shape for invalid bg-process actions. */
|
|
221
|
+
function errResult(action: DaemonDetails["action"], message: string): {
|
|
222
|
+
content: Array<{ type: "text"; text: string }>;
|
|
223
|
+
details: DaemonDetails;
|
|
224
|
+
isError: true;
|
|
225
|
+
} {
|
|
226
|
+
return {
|
|
227
|
+
content: [{ type: "text", text: message }],
|
|
228
|
+
details: { action, ok: false },
|
|
229
|
+
isError: true,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Build the background-process capability, binding it to a fresh per-session
|
|
235
|
+
* {@link DaemonTable} scoped to the context's working directory.
|
|
236
|
+
*
|
|
237
|
+
* @param ctx the deck context — its `cwd` is where launched processes run.
|
|
238
|
+
*/
|
|
239
|
+
export function buildDaemonCapability(ctx: DeckContext): Capability<typeof DaemonParams, DaemonDetails> {
|
|
240
|
+
const table = new DaemonTable(ctx.cwd);
|
|
241
|
+
return {
|
|
242
|
+
name: "bg-process",
|
|
243
|
+
label: "Background process",
|
|
244
|
+
description: DAEMON_DESCRIPTION,
|
|
245
|
+
parameters: DaemonParams,
|
|
246
|
+
async execute(_toolCallId, params) {
|
|
247
|
+
switch (params.action) {
|
|
248
|
+
case "start": {
|
|
249
|
+
if (!params.command) {
|
|
250
|
+
return errResult("start", "`command` is required to start a background process.");
|
|
251
|
+
}
|
|
252
|
+
const h = table.start(params.command);
|
|
253
|
+
return {
|
|
254
|
+
content: [
|
|
255
|
+
{
|
|
256
|
+
type: "text",
|
|
257
|
+
text: `Started background process ${h.id}: ${h.command}\nPoll it with action:"poll", id:"${h.id}".`,
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
details: { action: "start", ok: true, id: h.id, state: h.state },
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
case "poll": {
|
|
264
|
+
const h = params.id ? table.get(params.id) : undefined;
|
|
265
|
+
if (!h) return errResult("poll", `No background process with id "${params.id ?? ""}".`);
|
|
266
|
+
const out = drainBuffer(h.stdout, params.sinceLast ?? false);
|
|
267
|
+
const err = drainBuffer(h.stderr, params.sinceLast ?? false);
|
|
268
|
+
const parts = [`Process ${h.id} \u2014 ${statusLine(h)}`];
|
|
269
|
+
if (out.length) parts.push(`--- stdout ---\n${out.join("\n")}`);
|
|
270
|
+
if (err.length) parts.push(`--- stderr ---\n${err.join("\n")}`);
|
|
271
|
+
if (out.length === 0 && err.length === 0) parts.push("(no new output)");
|
|
272
|
+
return {
|
|
273
|
+
content: [{ type: "text", text: parts.join("\n") }],
|
|
274
|
+
details: {
|
|
275
|
+
action: "poll",
|
|
276
|
+
ok: true,
|
|
277
|
+
id: h.id,
|
|
278
|
+
state: h.state,
|
|
279
|
+
exitCode: h.exitCode,
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
case "stop": {
|
|
284
|
+
const h = params.id ? table.get(params.id) : undefined;
|
|
285
|
+
if (!h) return errResult("stop", `No background process with id "${params.id ?? ""}".`);
|
|
286
|
+
await table.stop(h);
|
|
287
|
+
return {
|
|
288
|
+
content: [
|
|
289
|
+
{
|
|
290
|
+
type: "text",
|
|
291
|
+
text: `Stopped background process ${h.id} (${statusLine(h)}).`,
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
details: {
|
|
295
|
+
action: "stop",
|
|
296
|
+
ok: true,
|
|
297
|
+
id: h.id,
|
|
298
|
+
state: h.state,
|
|
299
|
+
exitCode: h.exitCode,
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
case "list": {
|
|
304
|
+
const all = table.list();
|
|
305
|
+
const text =
|
|
306
|
+
all.length === 0
|
|
307
|
+
? "No background processes are being tracked."
|
|
308
|
+
: all.map((h) => `${h.id} [${statusLine(h)}] ${h.command}`).join("\n");
|
|
309
|
+
return {
|
|
310
|
+
content: [{ type: "text", text }],
|
|
311
|
+
details: {
|
|
312
|
+
action: "list",
|
|
313
|
+
ok: true,
|
|
314
|
+
processes: all.map((h) => ({
|
|
315
|
+
id: h.id,
|
|
316
|
+
command: h.command,
|
|
317
|
+
state: h.state,
|
|
318
|
+
})),
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Catalog row for the background-process capability. */
|
|
328
|
+
export const daemonCard: CapabilityCard = {
|
|
329
|
+
id: capabilityId("bg-process"),
|
|
330
|
+
title: "Background process",
|
|
331
|
+
summary: "Start, poll, and stop long-lived child processes that span multiple turns.",
|
|
332
|
+
// Erase the precisely-typed builder to the `Capability` card surface (through
|
|
333
|
+
// `unknown`, as TypeBox tools are invariant in their parameter schema).
|
|
334
|
+
build: (ctx) => buildDaemonCapability(ctx),
|
|
335
|
+
};
|