typeclaw 0.36.8 → 0.37.1
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/README.md +3 -3
- package/package.json +3 -2
- package/src/agent/index.ts +31 -11
- package/src/agent/live-sessions.ts +12 -0
- package/src/agent/model-fallback.ts +17 -15
- package/src/agent/model-overrides.ts +2 -2
- package/src/agent/session-meta.ts +10 -0
- package/src/agent/subagents.ts +30 -3
- package/src/agent/system-prompt.ts +9 -3
- package/src/agent/todo/continuation-policy.ts +6 -3
- package/src/agent/todo/continuation-wiring.ts +4 -2
- package/src/agent/todo/continuation.ts +3 -3
- package/src/agent/tools/todo/index.ts +27 -4
- package/src/bundled-plugins/agent-browser/index.ts +33 -108
- package/src/bundled-plugins/agent-browser/shim.ts +3 -94
- package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +8 -33
- package/src/bundled-plugins/doc-render/skills/typeclaw-render-pdf/SKILL.md +2 -2
- package/src/bundled-plugins/guard/policies/memory-retrieval-cache-write.ts +7 -1
- package/src/bundled-plugins/memory/README.md +80 -23
- package/src/bundled-plugins/memory/append-tool.ts +74 -53
- package/src/bundled-plugins/memory/citation-superset.ts +4 -0
- package/src/bundled-plugins/memory/citations.ts +54 -0
- package/src/bundled-plugins/memory/dreaming-metrics.ts +30 -0
- package/src/bundled-plugins/memory/dreaming.ts +444 -21
- package/src/bundled-plugins/memory/index.ts +544 -400
- package/src/bundled-plugins/memory/load-memory.ts +87 -10
- package/src/bundled-plugins/memory/load-shards.ts +48 -22
- package/src/bundled-plugins/memory/memory-logger.ts +95 -106
- package/src/bundled-plugins/memory/memory-retrieval.ts +3 -3
- package/src/bundled-plugins/memory/parent-link.ts +33 -0
- package/src/bundled-plugins/memory/paths.ts +12 -0
- package/src/bundled-plugins/memory/references/frontmatter.ts +197 -0
- package/src/bundled-plugins/memory/references/load-references.ts +212 -0
- package/src/bundled-plugins/memory/references/store-reference-tool.ts +59 -0
- package/src/bundled-plugins/memory/search-tool.ts +282 -45
- package/src/bundled-plugins/memory/stream-events.ts +1 -0
- package/src/bundled-plugins/memory/stream-io.ts +28 -3
- package/src/bundled-plugins/memory/turn-dedup.ts +40 -0
- package/src/bundled-plugins/memory/vector/cache-write.ts +19 -0
- package/src/bundled-plugins/memory/vector/config.ts +28 -0
- package/src/bundled-plugins/memory/vector/doctor.ts +124 -0
- package/src/bundled-plugins/memory/vector/embedder.ts +246 -0
- package/src/bundled-plugins/memory/vector/hybrid.ts +439 -0
- package/src/bundled-plugins/memory/vector/index-on-write.ts +34 -0
- package/src/bundled-plugins/memory/vector/inspect.ts +111 -0
- package/src/bundled-plugins/memory/vector/passages.ts +125 -0
- package/src/bundled-plugins/memory/vector/reference-index-on-write.ts +50 -0
- package/src/bundled-plugins/memory/vector/relevance-gate.ts +93 -0
- package/src/bundled-plugins/memory/vector/startup.ts +71 -0
- package/src/bundled-plugins/memory/vector/store.ts +203 -0
- package/src/bundled-plugins/memory/vector/truncation.ts +124 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +2 -0
- package/src/channels/router.ts +239 -40
- package/src/cli/incomplete-init.ts +57 -0
- package/src/cli/init.ts +166 -18
- package/src/cli/inspect.ts +11 -5
- package/src/cli/model.ts +115 -36
- package/src/cli/provider.ts +5 -3
- package/src/cli/restart.ts +24 -0
- package/src/cli/start.ts +24 -0
- package/src/cli/tunnel.ts +53 -8
- package/src/config/config.ts +110 -19
- package/src/config/index.ts +5 -1
- package/src/config/models-mutation.ts +29 -11
- package/src/config/providers-mutation.ts +2 -2
- package/src/config/providers.ts +146 -12
- package/src/container/shared.ts +9 -0
- package/src/container/start.ts +87 -4
- package/src/cron/consumer.ts +13 -7
- package/src/hostd/models.ts +64 -0
- package/src/hostd/paths.ts +6 -0
- package/src/hostd/portbroker-manager.ts +2 -2
- package/src/init/checkpoint.ts +201 -0
- package/src/init/dockerfile.ts +121 -34
- package/src/init/gitignore.ts +7 -7
- package/src/init/index.ts +41 -9
- package/src/init/models-dev.ts +96 -21
- package/src/init/oauth-login.ts +3 -3
- package/src/init/progress.ts +29 -0
- package/src/init/validate-api-key.ts +4 -0
- package/src/inspect/index.ts +13 -6
- package/src/inspect/item-list.ts +11 -2
- package/src/inspect/live-list.ts +65 -0
- package/src/inspect/open-item.ts +22 -1
- package/src/inspect/session-list.ts +29 -0
- package/src/models/embedding-model.ts +114 -0
- package/src/models/transformers-version.ts +55 -0
- package/src/plugin/types.ts +3 -0
- package/src/portbroker/container-server.ts +23 -0
- package/src/portbroker/forward-request-bus.ts +35 -0
- package/src/portbroker/forward-result-bus.ts +2 -3
- package/src/portbroker/hostd-client.ts +182 -36
- package/src/portbroker/index.ts +6 -1
- package/src/portbroker/protocol.ts +9 -2
- package/src/run/channel-session-factory.ts +11 -1
- package/src/run/index.ts +65 -8
- package/src/server/command-runner.ts +24 -1
- package/src/server/index.ts +42 -8
- package/src/shared/index.ts +2 -0
- package/src/shared/protocol.ts +31 -0
- package/src/skills/typeclaw-channels/SKILL.md +4 -4
- package/src/skills/typeclaw-config/SKILL.md +2 -2
- package/src/skills/typeclaw-memory/SKILL.md +3 -1
- package/src/skills/typeclaw-permissions/SKILL.md +3 -3
- package/src/skills/typeclaw-skills/SKILL.md +1 -1
- package/src/skills/typeclaw-tunnels/SKILL.md +22 -1
- package/src/tunnels/providers/cloudflare-quick.ts +65 -7
- package/src/tunnels/upstream-probe.ts +25 -0
- package/typeclaw.schema.json +156 -67
- package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +0 -170
- package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +0 -421
- package/src/portbroker/bind-with-forward.ts +0 -102
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<img src="./docs/public/typeclaw.png" alt="TypeClaw logo" width="240" />
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
|
-
>
|
|
7
|
+
> The agent for perfectionists — crafted in every detail. It behaves in your team's chat and gets sharper the longer it runs. Sandboxed and self-managing.
|
|
8
8
|
|
|
9
9
|
## Why?
|
|
10
10
|
|
|
@@ -31,7 +31,7 @@ If you're like me, TypeClaw is the right choice. If not, that's fine too.
|
|
|
31
31
|
|
|
32
32
|
- 🐳 **Sandboxed by default** — every agent runs in its own Docker container with `.env` injection and bind-mounted host folders
|
|
33
33
|
- 🔌 **Plugin system** — plain TypeScript modules contribute tools, skills, subagents, channels, commands, and typed config
|
|
34
|
-
- 💬 **Multi-channel** — Slack, Discord, Telegram, KakaoTalk, GitHub webhooks, and a websocket TUI; one agent, many inboxes
|
|
34
|
+
- 💬 **Multi-channel** — Slack, Discord, Telegram, LINE, KakaoTalk, GitHub webhooks, and a websocket TUI; one agent, many inboxes
|
|
35
35
|
- ⏰ **Cron** — schedule prompts or shell commands; per-job coalescing so slow jobs don't pile up
|
|
36
36
|
- 📚 **Skills on demand** — markdown procedures the agent loads only when relevant; zero token cost until used
|
|
37
37
|
- 🔎 **Web research** — bundled `scout` subagent plus first-class `web_search` and `web_fetch` tools (DuckDuckGo via curl-impersonate, Wikipedia)
|
|
@@ -97,7 +97,7 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for the recommended local dev loop (`bu
|
|
|
97
97
|
|
|
98
98
|
## Acknowledgments
|
|
99
99
|
|
|
100
|
-
- **Multi-channel** is powered by [agent-messenger](https://github.com/agent-messenger/agent-messenger) — every non-GitHub adapter (`slack-bot`, `discord-bot`, `telegram-bot`, `kakaotalk`) is built on its SDK. Thanks to the maintainers for the credential extraction, listener protocols, and platform coverage that made multi-channel a feature instead of a year-long project.
|
|
100
|
+
- **Multi-channel** is powered by [agent-messenger](https://github.com/agent-messenger/agent-messenger) — every non-GitHub adapter (`slack-bot`, `discord-bot`, `telegram-bot`, `line`, `kakaotalk`) is built on its SDK. Thanks to the maintainers for the credential extraction, listener protocols, and platform coverage that made multi-channel a feature instead of a year-long project.
|
|
101
101
|
- **Subagent architecture** is inspired by [oh-my-openagent](https://github.com/code-yeongyu/oh-my-openagent) by [@code-yeongyu](https://github.com/code-yeongyu). Thanks for the shape that made this clean.
|
|
102
102
|
|
|
103
103
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typeclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.1",
|
|
4
4
|
"homepage": "https://github.com/typeclaw/typeclaw#readme",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/typeclaw/typeclaw/issues"
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@clack/core": "^1.2.0",
|
|
46
46
|
"@clack/prompts": "^1.2.0",
|
|
47
|
+
"@huggingface/transformers": "4.2.0",
|
|
47
48
|
"@mariozechner/pi-coding-agent": "^0.67.3",
|
|
48
49
|
"@mariozechner/pi-tui": "^0.67.3",
|
|
49
50
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
"cron-parser": "^5.5.0",
|
|
55
56
|
"jq-wasm": "^1.1.0-jq-1.8.1",
|
|
56
57
|
"jsdom": "^29.0.2",
|
|
58
|
+
"proper-lockfile": "^4.1.2",
|
|
57
59
|
"qrcode": "^1.5.4",
|
|
58
60
|
"turndown": "^7.2.4",
|
|
59
61
|
"zod": "^4.3.6"
|
|
@@ -66,7 +68,6 @@
|
|
|
66
68
|
"@types/qrcode": "^1.5.6",
|
|
67
69
|
"@types/sinonjs__fake-timers": "^15.0.1",
|
|
68
70
|
"@types/turndown": "^5.0.6",
|
|
69
|
-
"@types/ws": "^8.18.1",
|
|
70
71
|
"@typescript/native-preview": "^7.0.0-dev.20260416.1",
|
|
71
72
|
"oxfmt": "^0.45.0",
|
|
72
73
|
"oxlint": "^1.60.0"
|
package/src/agent/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { loadMemory } from '@/bundled-plugins/memory/load-memory'
|
|
|
15
15
|
import type { ChannelRouter } from '@/channels/router'
|
|
16
16
|
import type { ReactionRef } from '@/channels/types'
|
|
17
17
|
import { getConfig, resolveModel, resolveProfile } from '@/config'
|
|
18
|
-
import { defaultThinkingLevelForRef, providerForModelRef, type
|
|
18
|
+
import { defaultThinkingLevelForRef, providerForModelRef, type ModelRef } from '@/config/providers'
|
|
19
19
|
import { renderMcpCatalog } from '@/mcp/catalog'
|
|
20
20
|
import type { McpManager } from '@/mcp/manager'
|
|
21
21
|
import { createMcpDispatcherTools, MCP_DISPATCHER_TOOL_NAMES } from '@/mcp/tools'
|
|
@@ -196,7 +196,7 @@ export type CreateSessionOptions = {
|
|
|
196
196
|
// pinned to the next ref in the chain after the previous one failed. When
|
|
197
197
|
// set, `profile` is still recorded for the fallback-warning bookkeeping;
|
|
198
198
|
// the profile→refs resolution is skipped.
|
|
199
|
-
refOverride?:
|
|
199
|
+
refOverride?: ModelRef
|
|
200
200
|
// Defensive ceiling on cumulative bytes of tool-result text per session,
|
|
201
201
|
// applied to the named tools only. See `src/agent/tool-result-budget.ts`
|
|
202
202
|
// for the rationale. Intended for subagents that read large files
|
|
@@ -221,6 +221,12 @@ export type CreateSessionOptions = {
|
|
|
221
221
|
subagentRegistry?: SubagentRegistry
|
|
222
222
|
createSessionForSubagent?: CreateSessionForSubagent
|
|
223
223
|
allowBackgroundFromSubagent?: boolean
|
|
224
|
+
// When true, the `# Memory` section is omitted from the system prompt and
|
|
225
|
+
// long-term memory is injected per-turn into the user prompt instead (the
|
|
226
|
+
// memory plugin's vector `session.turn.start` path). Derived once at boot
|
|
227
|
+
// from `memory.vector.enabled`, which is restart-required — so the boot
|
|
228
|
+
// snapshot stays coherent with the per-turn injection decision.
|
|
229
|
+
suppressSystemMemory?: boolean
|
|
224
230
|
}
|
|
225
231
|
|
|
226
232
|
export type CreateSessionResult = {
|
|
@@ -241,7 +247,7 @@ export async function createSessionWithDispose(options: CreateSessionOptions = {
|
|
|
241
247
|
// exactly what they're doing.
|
|
242
248
|
// `refOverride` lets the model-fallback helper pin a specific entry from
|
|
243
249
|
// the chain when it recreates a session after the previous ref failed.
|
|
244
|
-
const activeRef:
|
|
250
|
+
const activeRef: ModelRef = options.refOverride ?? resolved.ref
|
|
245
251
|
const { authStorage, modelRegistry } = getAuthFor(providerForModelRef(activeRef))
|
|
246
252
|
|
|
247
253
|
const materializedSkills =
|
|
@@ -270,6 +276,7 @@ export async function createSessionWithDispose(options: CreateSessionOptions = {
|
|
|
270
276
|
...(options.runtimeVersion !== undefined ? { runtimeVersion: options.runtimeVersion } : {}),
|
|
271
277
|
...(options.mcpManager !== undefined ? { mcpManager: options.mcpManager } : {}),
|
|
272
278
|
...(options.subagentRegistry !== undefined ? { subagentRegistry: options.subagentRegistry } : {}),
|
|
279
|
+
...(options.suppressSystemMemory !== undefined ? { suppressSystemMemory: options.suppressSystemMemory } : {}),
|
|
273
280
|
})
|
|
274
281
|
|
|
275
282
|
const getOrigin: () => SessionOrigin | undefined =
|
|
@@ -944,6 +951,12 @@ export type CreateResourceLoaderOptions = {
|
|
|
944
951
|
// 'full' to force the heavy prompt even on an unattended origin (rarely
|
|
945
952
|
// useful; mostly an escape hatch for ad-hoc debugging).
|
|
946
953
|
mode?: SystemPromptMode
|
|
954
|
+
// When true, the `# Memory` section is omitted from the system prompt and
|
|
955
|
+
// long-term memory is injected per-turn into the user prompt instead (the
|
|
956
|
+
// memory plugin's vector `session.turn.start` path). Derived once at boot
|
|
957
|
+
// from `memory.vector.enabled` — vector is restart-required, so the boot
|
|
958
|
+
// snapshot is coherent with the per-turn injection decision.
|
|
959
|
+
suppressSystemMemory?: boolean
|
|
947
960
|
}
|
|
948
961
|
|
|
949
962
|
// Origins where the operator-facing DEFAULT_SYSTEM_PROMPT, git-nudge, and the
|
|
@@ -1024,8 +1037,8 @@ export type SystemPromptComposition = {
|
|
|
1024
1037
|
// memory/ after every turn, so the dirty-files list is empty most of
|
|
1025
1038
|
// the time.
|
|
1026
1039
|
// 3. memorySection — volatile: MEMORY.md grows on every dream cycle and
|
|
1027
|
-
// memory/yyyy-MM-dd.
|
|
1028
|
-
// memory-logger.
|
|
1040
|
+
// memory/streams/yyyy-MM-dd.jsonl grows after every channel turn that
|
|
1041
|
+
// triggers memory-logger.
|
|
1029
1042
|
//
|
|
1030
1043
|
// The wall-clock anchor that used to live here as `## Now` moved out
|
|
1031
1044
|
// entirely. It is now injected into the user turn at each `session.prompt`
|
|
@@ -1099,12 +1112,19 @@ export async function createResourceLoader(options: CreateResourceLoaderOptions
|
|
|
1099
1112
|
// gather point.
|
|
1100
1113
|
const selfPromise = loadSelf(agentDir)
|
|
1101
1114
|
const gitNudgeSettled = mode === 'slim' ? Promise.resolve(ok('')) : settle(renderGitNudge(agentDir))
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1115
|
+
// Vector agents omit the `# Memory` section entirely: long-term memory is
|
|
1116
|
+
// injected per-turn into the user prompt by the memory plugin's vector
|
|
1117
|
+
// `session.turn.start` hook. Keeping both would double-inject and re-break the
|
|
1118
|
+
// cache prefix this change exists to protect — the invariant is
|
|
1119
|
+
// `suppressSystemMemory === memory.vector.enabled`.
|
|
1120
|
+
const memorySettled = options.suppressSystemMemory
|
|
1121
|
+
? Promise.resolve(ok(''))
|
|
1122
|
+
: settle(
|
|
1123
|
+
loadMemory(agentDir, {
|
|
1124
|
+
...(options.origin !== undefined ? { origin: options.origin } : {}),
|
|
1125
|
+
...(options.plugins?.sessionId !== undefined ? { currentSessionId: options.plugins.sessionId } : {}),
|
|
1126
|
+
}),
|
|
1127
|
+
)
|
|
1108
1128
|
|
|
1109
1129
|
let self = await selfPromise
|
|
1110
1130
|
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type { AgentSession } from './index'
|
|
2
|
+
import type { MinimalSessionOrigin } from './session-meta'
|
|
2
3
|
|
|
3
4
|
export type LiveAgentSession = {
|
|
4
5
|
sessionId: string
|
|
5
6
|
session: Pick<AgentSession, 'subscribe'>
|
|
7
|
+
// Surfaced by the inspect picker for sessions not yet on disk: pi-coding-agent
|
|
8
|
+
// defers the first .jsonl write until the first assistant message, so without
|
|
9
|
+
// these a mid-reply session is invisible. Optional so subscribe-only test
|
|
10
|
+
// harnesses can still register `{ sessionId, session }`; live-listing skips
|
|
11
|
+
// entries lacking an origin.
|
|
12
|
+
origin?: MinimalSessionOrigin
|
|
13
|
+
registeredAtMs?: number
|
|
6
14
|
}
|
|
7
15
|
|
|
8
16
|
export class LiveSessionRegistry {
|
|
@@ -24,6 +32,10 @@ export class LiveSessionRegistry {
|
|
|
24
32
|
return this.entries.has(sessionId)
|
|
25
33
|
}
|
|
26
34
|
|
|
35
|
+
listLive(): LiveAgentSession[] {
|
|
36
|
+
return [...this.entries.values()].filter((e) => e.origin !== undefined)
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
size(): number {
|
|
28
40
|
return this.entries.size
|
|
29
41
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveProfile } from '@/config'
|
|
2
2
|
import type { Models } from '@/config/config'
|
|
3
|
-
import type { KnownModelRef } from '@/config/providers'
|
|
3
|
+
import type { KnownModelRef, ModelRef } from '@/config/providers'
|
|
4
4
|
|
|
5
5
|
import type { AgentSession } from './index'
|
|
6
6
|
import { subscribeProviderErrors } from './provider-error'
|
|
@@ -15,18 +15,20 @@ import { renderTurnTimeAnchor } from './system-prompt'
|
|
|
15
15
|
// the final entry, on full-chain failure). Callers that need to keep using
|
|
16
16
|
// the session for subsequent turns store these in their state; callers that
|
|
17
17
|
// tear down per-turn (cron) just call `dispose()` and discard.
|
|
18
|
-
|
|
18
|
+
type FallbackModelRef = KnownModelRef | ModelRef
|
|
19
|
+
|
|
20
|
+
export type FallbackPromptResult<TRef extends FallbackModelRef = ModelRef> = {
|
|
19
21
|
success: boolean
|
|
20
|
-
refUsed:
|
|
21
|
-
attempts: FallbackAttempt[]
|
|
22
|
+
refUsed: TRef
|
|
23
|
+
attempts: FallbackAttempt<TRef>[]
|
|
22
24
|
session: AgentSession
|
|
23
25
|
dispose: () => Promise<void>
|
|
24
26
|
// When `success === false`, this is the error from the final attempt.
|
|
25
27
|
lastError?: Error
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
export type FallbackAttempt = {
|
|
29
|
-
ref:
|
|
30
|
+
export type FallbackAttempt<TRef extends FallbackModelRef = ModelRef> = {
|
|
31
|
+
ref: TRef
|
|
30
32
|
// 'hard' = session.prompt() threw. 'soft' = pi-coding-agent surfaced an
|
|
31
33
|
// upstream error via stopReason: 'error' on the final assistant message.
|
|
32
34
|
// 'success' = the turn finished cleanly.
|
|
@@ -40,7 +42,7 @@ export type FallbackAttempt = {
|
|
|
40
42
|
//
|
|
41
43
|
// Exported so callers can introspect the chain (e.g. logs, telemetry) before
|
|
42
44
|
// firing the prompt — useful for `[cron] ${jobId}: trying chain a → b → c`.
|
|
43
|
-
export function resolveFallbackChain(models: Models, profile: string | undefined):
|
|
45
|
+
export function resolveFallbackChain(models: Models, profile: string | undefined): ModelRef[] {
|
|
44
46
|
return resolveProfile(models, profile).refs
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -62,18 +64,18 @@ export function resolveFallbackChain(models: Models, profile: string | undefined
|
|
|
62
64
|
// (console.error in the server drain, channel reaction in the router,
|
|
63
65
|
// cron-job status). This keeps the helper composable with the existing
|
|
64
66
|
// error-handling code at each call site.
|
|
65
|
-
export async function promptWithFallback(opts: {
|
|
66
|
-
refs:
|
|
67
|
+
export async function promptWithFallback<TRef extends FallbackModelRef>(opts: {
|
|
68
|
+
refs: TRef[]
|
|
67
69
|
text: string
|
|
68
|
-
createSessionForRef: (ref:
|
|
70
|
+
createSessionForRef: (ref: TRef) => Promise<{ session: AgentSession; dispose: () => Promise<void> }>
|
|
69
71
|
// Called after each non-final attempt so callers can log the per-attempt
|
|
70
72
|
// failure with their own context (sessionId, channel key, job id, ...).
|
|
71
|
-
onAttemptFailed?: (attempt: FallbackAttempt) => void
|
|
72
|
-
}): Promise<FallbackPromptResult
|
|
73
|
+
onAttemptFailed?: (attempt: FallbackAttempt<TRef>) => void
|
|
74
|
+
}): Promise<FallbackPromptResult<TRef>> {
|
|
73
75
|
if (opts.refs.length === 0) {
|
|
74
76
|
throw new Error('promptWithFallback: refs[] must be non-empty')
|
|
75
77
|
}
|
|
76
|
-
const attempts: FallbackAttempt[] = []
|
|
78
|
+
const attempts: FallbackAttempt<TRef>[] = []
|
|
77
79
|
let lastError: Error | undefined
|
|
78
80
|
for (let i = 0; i < opts.refs.length; i++) {
|
|
79
81
|
const ref = opts.refs[i]!
|
|
@@ -92,7 +94,7 @@ export async function promptWithFallback(opts: {
|
|
|
92
94
|
await session.prompt(`${renderTurnTimeAnchor()}\n\n${opts.text}`)
|
|
93
95
|
} catch (err) {
|
|
94
96
|
const error = err instanceof Error ? err : new Error(String(err))
|
|
95
|
-
const attempt: FallbackAttempt = { ref, outcome: 'hard', errorMessage: error.message }
|
|
97
|
+
const attempt: FallbackAttempt<TRef> = { ref, outcome: 'hard', errorMessage: error.message }
|
|
96
98
|
attempts.push(attempt)
|
|
97
99
|
lastError = error
|
|
98
100
|
if (!isLast) opts.onAttemptFailed?.(attempt)
|
|
@@ -104,7 +106,7 @@ export async function promptWithFallback(opts: {
|
|
|
104
106
|
continue
|
|
105
107
|
}
|
|
106
108
|
if (softError !== undefined) {
|
|
107
|
-
const attempt: FallbackAttempt = { ref, outcome: 'soft', errorMessage: softError.message }
|
|
109
|
+
const attempt: FallbackAttempt<TRef> = { ref, outcome: 'soft', errorMessage: softError.message }
|
|
108
110
|
attempts.push(attempt)
|
|
109
111
|
lastError = softError
|
|
110
112
|
if (!isLast) opts.onAttemptFailed?.(attempt)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Api, Model } from '@mariozechner/pi-ai'
|
|
2
2
|
|
|
3
|
-
import { providerForModelRef, type KnownModelRef, type KnownProviderId } from '@/config/providers'
|
|
3
|
+
import { providerForModelRef, type KnownModelRef, type KnownProviderId, type ModelRef } from '@/config/providers'
|
|
4
4
|
|
|
5
5
|
// Providers whose base URL can be swapped to an upstream-compatible gateway at
|
|
6
6
|
// runtime. Each env var mirrors the upstream SDK's own name so a credential /
|
|
@@ -26,7 +26,7 @@ type OverridableProviderId = keyof typeof PROVIDER_BASE_URL_ENV
|
|
|
26
26
|
// data that must never be mutated.
|
|
27
27
|
export function applyModelRuntimeOverrides<TApi extends Api>(
|
|
28
28
|
model: Model<TApi>,
|
|
29
|
-
ref: KnownModelRef,
|
|
29
|
+
ref: KnownModelRef | ModelRef | string,
|
|
30
30
|
env: NodeJS.ProcessEnv = process.env,
|
|
31
31
|
): Model<TApi> {
|
|
32
32
|
const providerId = providerForModelRef(ref)
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
import type { LiveSessionOriginPayload } from '@/shared'
|
|
2
|
+
|
|
1
3
|
import type { SessionOrigin } from './session-origin'
|
|
2
4
|
|
|
5
|
+
// Bidirectional structural equality with the wire mirror in @/shared/protocol.
|
|
6
|
+
// @/shared cannot import this module (it is a leaf), so the type cannot be
|
|
7
|
+
// shared directly; these assignments fail typecheck if either side drifts.
|
|
8
|
+
const _originIsWireCompatible: LiveSessionOriginPayload = null as unknown as MinimalSessionOrigin
|
|
9
|
+
const _wireIsOriginCompatible: MinimalSessionOrigin = null as unknown as LiveSessionOriginPayload
|
|
10
|
+
void _originIsWireCompatible
|
|
11
|
+
void _wireIsOriginCompatible
|
|
12
|
+
|
|
3
13
|
export const SESSION_META_CUSTOM_TYPE = 'typeclaw.session-meta'
|
|
4
14
|
|
|
5
15
|
export type SessionMetaPayload = {
|
package/src/agent/subagents.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { ToolDefinition } from '@mariozechner/pi-coding-agent'
|
|
2
2
|
import type { z } from 'zod'
|
|
3
3
|
|
|
4
|
+
import type { PermissionService } from '@/permissions'
|
|
4
5
|
import type { HookBus } from '@/plugin'
|
|
5
6
|
import type { Stream, Unsubscribe } from '@/stream'
|
|
6
7
|
|
|
7
|
-
import { type AgentSession, createSession } from './index'
|
|
8
|
+
import { type AgentSession, createSession, type PluginSessionWiring } from './index'
|
|
8
9
|
import { subscribeProviderErrors } from './provider-error'
|
|
9
10
|
import type { SubagentBashPolicy } from './reviewer-bash-policy'
|
|
10
11
|
import type { SessionOrigin } from './session-origin'
|
|
@@ -143,6 +144,21 @@ export type CreateSessionForSubagentOptions = {
|
|
|
143
144
|
parentSessionId?: string
|
|
144
145
|
spawnedByRole?: string
|
|
145
146
|
spawnedByOrigin?: SessionOrigin
|
|
147
|
+
// Plugin hook wiring for the subagent's tools. When present, the subagent's
|
|
148
|
+
// builtin bash/read/edit/write run through the plugin `tool.before`/`tool.after`
|
|
149
|
+
// hooks (security guards AND github-cli-auth GitHub-token injection) exactly
|
|
150
|
+
// like the main and plugin-subagent sessions. Without it, the builtin tools run
|
|
151
|
+
// raw (the prior behavior) — so standalone/test callers stay unaffected. The
|
|
152
|
+
// production runtime always supplies it (src/run/index.ts) so a generic
|
|
153
|
+
// task-spawned subagent's `git push`/`gh` gets a minted token instead of
|
|
154
|
+
// failing with "could not read Username".
|
|
155
|
+
plugins?: PluginSessionWiring
|
|
156
|
+
// The role/permission service that drives builtin-bash sandboxing. It MUST be
|
|
157
|
+
// forwarded alongside `plugins`: buildBuiltinPiToolOverrides only applies
|
|
158
|
+
// applyBashSandbox / applyTmpPathRedirect when `permissions` is present, so
|
|
159
|
+
// wiring hooks without permissions would inject the GitHub token yet leave the
|
|
160
|
+
// sandbox OFF — strictly weaker than the plugin-subagent branch this matches.
|
|
161
|
+
permissions?: PermissionService
|
|
146
162
|
}
|
|
147
163
|
export type CreateSessionForSubagent = (
|
|
148
164
|
subagent: Subagent<any>,
|
|
@@ -161,6 +177,8 @@ export const defaultCreateSessionForSubagent: CreateSessionForSubagent = (subage
|
|
|
161
177
|
},
|
|
162
178
|
...(subagent.tools ? { tools: subagent.tools } : {}),
|
|
163
179
|
customTools: subagent.customTools ?? [],
|
|
180
|
+
...(options?.plugins !== undefined ? { plugins: options.plugins } : {}),
|
|
181
|
+
...(options?.permissions !== undefined ? { permissions: options.permissions } : {}),
|
|
164
182
|
...(subagent.profile !== undefined ? { profile: subagent.profile } : {}),
|
|
165
183
|
...(subagent.toolResultBudget !== undefined ? { toolResultBudget: subagent.toolResultBudget } : {}),
|
|
166
184
|
...(subagent.bashPolicy !== undefined ? { bashPolicy: subagent.bashPolicy } : {}),
|
|
@@ -262,15 +280,24 @@ export async function invokeSubagent(name: string, options: InvokeSubagentOption
|
|
|
262
280
|
? { sessionId, agentDir, ...(origin !== undefined ? { origin } : {}) }
|
|
263
281
|
: undefined
|
|
264
282
|
const userPromptForTurn = override?.userPrompt ?? options.userPrompt
|
|
283
|
+
// Per-turn memory injection for vector agents: subagents have no
|
|
284
|
+
// system-prompt `# Memory` section (their prompt is a systemPromptOverride),
|
|
285
|
+
// so the turn-start hook renders memory into `retrievalContext.results`,
|
|
286
|
+
// appended to the user turn below. Empty for non-vector agents.
|
|
287
|
+
const retrievalContext = { results: '' }
|
|
265
288
|
try {
|
|
266
289
|
if (hooks && turnEvent !== undefined) {
|
|
267
|
-
await hooks.runSessionTurnStart({ ...turnEvent, userPrompt: userPromptForTurn })
|
|
290
|
+
await hooks.runSessionTurnStart({ ...turnEvent, userPrompt: userPromptForTurn, retrievalContext })
|
|
268
291
|
}
|
|
269
292
|
if (backgroundDrain !== undefined) {
|
|
270
293
|
drainWatch = beginSubagentDrainWatch(backgroundDrain)
|
|
271
294
|
}
|
|
272
295
|
try {
|
|
273
|
-
|
|
296
|
+
const turnText =
|
|
297
|
+
retrievalContext.results.length > 0
|
|
298
|
+
? `${renderTurnTimeAnchor()}\n\n${userPromptForTurn}\n\n${retrievalContext.results}`
|
|
299
|
+
: `${renderTurnTimeAnchor()}\n\n${userPromptForTurn}`
|
|
300
|
+
await session.prompt(turnText)
|
|
274
301
|
} finally {
|
|
275
302
|
if (hooks && turnEvent !== undefined) {
|
|
276
303
|
await hooks.runSessionTurnEnd(turnEvent)
|
|
@@ -56,7 +56,7 @@ When the user gives you work, start doing it in the same turn — a real action,
|
|
|
56
56
|
|
|
57
57
|
## Tracking your work
|
|
58
58
|
|
|
59
|
-
For any multi-step or long-running task, maintain a todo list with \`todo_write\` and mark items complete as you finish them. This is not bookkeeping for its own sake: if this session is interrupted — a restart, a crash, or simply a later turn — the runtime uses the remaining incomplete items to resume the work instead of silently dropping it. Write the list when you start the work
|
|
59
|
+
For any multi-step or long-running task, maintain a todo list with \`todo_write\` and mark items complete as you finish them. This is not bookkeeping for its own sake: if this session is interrupted — a restart, a crash, or simply a later turn — the runtime uses the remaining incomplete items to resume the work instead of silently dropping it. Write the list when you start the work and update statuses as you go; once your \`todo_write\` leaves no incomplete items, the runtime clears the list for you. Use \`todo_clear\` only to abandon a task with items still incomplete. A single-step request needs no todo list.
|
|
60
60
|
|
|
61
61
|
## Tool-call style
|
|
62
62
|
|
|
@@ -81,13 +81,17 @@ Use this only when the work belongs in *your* session. For self-contained long w
|
|
|
81
81
|
|
|
82
82
|
## Version control
|
|
83
83
|
|
|
84
|
-
Your agent folder is a git repository.
|
|
84
|
+
Your agent folder is a git repository, but **it is your own private backup repo — not a software project you develop.** It exists so TypeClaw can snapshot your identity files, \`sessions/\`, and \`memory/\` over time. It has no GitHub remote, nothing is pushed anywhere, and it is **not** a checkout of any project's source code. So when you commit here, you are saving your own state — not contributing to a codebase.
|
|
85
|
+
|
|
86
|
+
This matters when the user asks you to work on an actual software project — fix a bug, build a feature, open a pull request. **That work does not happen in your agent folder.** Clone the project's repo somewhere else first (e.g. \`/tmp/<repo>\`), do the work there, and open the PR from that clone with \`gh\`. Never \`git init\`, add a remote, or try to push your agent folder as if it were the project — and if you can't find the project repo or its remote, ask the user where it lives instead of treating this folder as the project. The two are separate: this folder is *where you live*, the project clone is *where you work*.
|
|
87
|
+
|
|
88
|
+
Commits to your agent folder (your own state):
|
|
85
89
|
|
|
86
90
|
- Commit any files you created, edited, or deleted before declaring a task done. One logical change = one commit; split unrelated changes.
|
|
87
91
|
- Use \`git add <paths>\` (not \`git add -A\`). Imperative commit messages ("Update SOUL.md to be less formal"); explain *why* in the body if non-obvious.
|
|
88
92
|
- Never commit \`secrets.json\`, \`.env\`, or anything under \`workspace/\` — truly-ignored by design. \`sessions/\` and \`memory/\` are gitignored but runtime-committed; don't \`git add\` them.
|
|
89
93
|
- ${PACKAGE_JSON_INSTALL_RULE}
|
|
90
|
-
- Never \`git push\`, \`git reset --hard\`, \`git rebase\`, or rewrite remote history unless the user explicitly asks.
|
|
94
|
+
- Never \`git push\`, \`git reset --hard\`, \`git rebase\`, or rewrite remote history in this folder unless the user explicitly asks. (Pushing a project clone you made elsewhere to open a PR is fine when the user asked for the PR.)
|
|
91
95
|
|
|
92
96
|
## How to behave
|
|
93
97
|
|
|
@@ -259,4 +263,6 @@ ${PACKAGE_JSON_INSTALL_RULE}
|
|
|
259
263
|
|
|
260
264
|
Your free-write zone is \`workspace/\`. Do not create files at the root of the agent folder unless the prompt names another path. \`public/\` is the guest-visible zone — write there anything meant to be shared with an untrusted caller (a \`guest\`-role turn cannot read \`workspace/\` but can read \`public/\`). Do not edit \`memory/topics/\` directly — the dreaming subagent owns it; to capture something memorable, surface it in your reply or let the memory-logger append to \`memory/streams/\`. Never stage or commit \`secrets.json\`, \`.env\`, \`sessions/\`, \`memory/\`, or \`workspace/\` — those are runtime- or user-managed.
|
|
261
265
|
|
|
266
|
+
The agent folder is a private backup repo with no remote, not a project checkout. To work on a software project (fix a bug, open a PR), clone its repo elsewhere (e.g. \`/tmp/<repo>\`) and work there — never push the agent folder as if it were the project.
|
|
267
|
+
|
|
262
268
|
See the session-origin block below for what kind of session this is and what's expected of you.`
|
|
@@ -39,10 +39,13 @@ export type ContinuationEpisode = {
|
|
|
39
39
|
// The outcome of the most recently completed turn, recorded from the
|
|
40
40
|
// `message_end` subscription (authoritative) or a prompt `finally` fallback.
|
|
41
41
|
// `stopReason: 'unknown'` is the fail-closed value: an idle that sees it does
|
|
42
|
-
// not auto-inject.
|
|
42
|
+
// not auto-inject. `'length'` is a budget truncation (the turn ran out of
|
|
43
|
+
// output tokens, often mid-thinking) — a legitimate unfinished turn that the
|
|
44
|
+
// continuation budget/stagnation guards are designed to bound, so it is
|
|
45
|
+
// continuation-eligible, NOT fail-closed.
|
|
43
46
|
export type TurnOutcome = {
|
|
44
47
|
turnId: string
|
|
45
|
-
stopReason: 'stop' | 'aborted' | 'error' | 'unknown'
|
|
48
|
+
stopReason: 'stop' | 'length' | 'aborted' | 'error' | 'unknown'
|
|
46
49
|
endedAt: number
|
|
47
50
|
// Total tokens the just-completed turn consumed (from the assistant
|
|
48
51
|
// message's usage). Accumulated into the episode's cumulativeTokens so the
|
|
@@ -73,7 +76,7 @@ export function emptyContinuationState(): ContinuationState {
|
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
const STOP_REASONS = new Set<TurnOutcome['stopReason']>(['stop', 'aborted', 'error', 'unknown'])
|
|
79
|
+
const STOP_REASONS = new Set<TurnOutcome['stopReason']>(['stop', 'length', 'aborted', 'error', 'unknown'])
|
|
77
80
|
|
|
78
81
|
// Validate a persisted state object field-by-field and fail closed: any field
|
|
79
82
|
// that does not match the expected shape is dropped to its empty value rather
|
|
@@ -14,9 +14,11 @@ import { writeTodos } from './store'
|
|
|
14
14
|
|
|
15
15
|
// Map a pi `message_end` event's stopReason onto the TurnOutcome stopReason
|
|
16
16
|
// space. Anything we don't recognize collapses to 'unknown' so the idle path
|
|
17
|
-
// fails closed (no auto-injection on an outcome we can't classify).
|
|
17
|
+
// fails closed (no auto-injection on an outcome we can't classify). 'length'
|
|
18
|
+
// is preserved (not collapsed) because a budget-truncated turn is a legitimate
|
|
19
|
+
// unfinished turn the continuation guards should be allowed to resume.
|
|
18
20
|
export function classifyStopReason(raw: unknown): TurnOutcome['stopReason'] {
|
|
19
|
-
if (raw === 'stop' || raw === 'aborted' || raw === 'error') return raw
|
|
21
|
+
if (raw === 'stop' || raw === 'length' || raw === 'aborted' || raw === 'error') return raw
|
|
20
22
|
return 'unknown'
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -16,9 +16,9 @@ export const CONTINUATION_PROMPT = [
|
|
|
16
16
|
'cancelled) as you finish it by calling `todo_write` with the updated list. If',
|
|
17
17
|
'you believe all the work is already done, do not just assert it — re-examine',
|
|
18
18
|
'each remaining item skeptically, verify the work actually landed, and update',
|
|
19
|
-
'the list accordingly.
|
|
20
|
-
'
|
|
21
|
-
'work.',
|
|
19
|
+
'the list accordingly. Once your `todo_write` leaves no incomplete items, the',
|
|
20
|
+
'list is cleared for you automatically. Do not acknowledge or reply to this',
|
|
21
|
+
'notice; just continue the work.',
|
|
22
22
|
'',
|
|
23
23
|
'---',
|
|
24
24
|
'',
|
|
@@ -50,7 +50,8 @@ export function createTodoTools({ agentDir, getOrigin }: CreateTodoToolsOptions)
|
|
|
50
50
|
'(restart, crash, or a later turn), you can resume the remaining work instead of silently ' +
|
|
51
51
|
'dropping it. Mark items `completed` (or `cancelled`) as you finish them by writing the full ' +
|
|
52
52
|
'list again with updated statuses. This is a full replace, not a merge: include every item ' +
|
|
53
|
-
'you still care about on each call.'
|
|
53
|
+
'you still care about on each call. When the list you write has no incomplete items left, ' +
|
|
54
|
+
'the runtime clears it for you — no separate cleanup call is needed.',
|
|
54
55
|
parameters: Type.Object({
|
|
55
56
|
todos: Type.Array(TODO_ITEM, { description: 'The complete todo list. Replaces any prior list.' }),
|
|
56
57
|
}),
|
|
@@ -61,8 +62,28 @@ export function createTodoTools({ agentDir, getOrigin }: CreateTodoToolsOptions)
|
|
|
61
62
|
return { content: [{ type: 'text' as const, text: NO_SCOPE_NOTICE }], details }
|
|
62
63
|
}
|
|
63
64
|
const todos = params.todos as Todo[]
|
|
64
|
-
await writeTodos(agentDir, scope, todos)
|
|
65
65
|
const remaining = incompleteTodos(todos).length
|
|
66
|
+
|
|
67
|
+
// Collapse a fully-resolved list to empty in the SAME write that
|
|
68
|
+
// completed it, rather than relying on a follow-up todo_clear. That
|
|
69
|
+
// follow-up can be lost to an abort landing on the next turn, leaving a
|
|
70
|
+
// resolved list on disk (harmless to continuation, but it never gets
|
|
71
|
+
// cleaned up). Clearing here makes the cleanup race-free by construction.
|
|
72
|
+
if (remaining === 0 && todos.length > 0) {
|
|
73
|
+
await writeTodos(agentDir, scope, [])
|
|
74
|
+
const details: TodoToolDetails = { ok: true, total: todos.length, remaining: 0 }
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: 'text' as const,
|
|
79
|
+
text: `All ${todos.length} todo(s) done; list cleared.`,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
details,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await writeTodos(agentDir, scope, todos)
|
|
66
87
|
const details: TodoToolDetails = { ok: true, total: todos.length, remaining }
|
|
67
88
|
return {
|
|
68
89
|
content: [
|
|
@@ -100,8 +121,10 @@ export function createTodoTools({ agentDir, getOrigin }: CreateTodoToolsOptions)
|
|
|
100
121
|
name: 'todo_clear',
|
|
101
122
|
label: 'Clear Todos',
|
|
102
123
|
description:
|
|
103
|
-
'Empty your todo list for this session.
|
|
104
|
-
'
|
|
124
|
+
'Empty your todo list for this session. Use this only to abandon a task with items still ' +
|
|
125
|
+
'incomplete, so the runtime stops tracking pending work. A list with no incomplete items ' +
|
|
126
|
+
'left is cleared automatically by `todo_write`, so you do not need to call this after ' +
|
|
127
|
+
'finishing everything.',
|
|
105
128
|
parameters: Type.Object({}),
|
|
106
129
|
async execute() {
|
|
107
130
|
const scope = scopeForOrigin(getOrigin)
|