typeclaw 0.3.0 → 0.4.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/README.md +20 -15
- package/auth.schema.json +113 -0
- package/package.json +2 -1
- package/scripts/dump-system-prompt.ts +401 -0
- package/secrets.schema.json +113 -0
- package/src/agent/index.ts +149 -30
- package/src/agent/provider-error.ts +44 -0
- package/src/agent/session-meta.ts +43 -0
- package/src/agent/session-origin.ts +3 -2
- package/src/agent/subagents.ts +8 -0
- package/src/agent/system-prompt.ts +70 -35
- package/src/bundled-plugins/security/index.ts +3 -2
- package/src/channels/adapters/github/auth-app.ts +120 -0
- package/src/channels/adapters/github/auth-pat.ts +50 -0
- package/src/channels/adapters/github/auth.ts +33 -0
- package/src/channels/adapters/github/channel-resolver.ts +30 -0
- package/src/channels/adapters/github/dedup.ts +26 -0
- package/src/channels/adapters/github/event-allowlist.ts +8 -0
- package/src/channels/adapters/github/fetch-attachment.ts +5 -0
- package/src/channels/adapters/github/history.ts +63 -0
- package/src/channels/adapters/github/inbound.ts +286 -0
- package/src/channels/adapters/github/index.ts +286 -0
- package/src/channels/adapters/github/managed-path.ts +54 -0
- package/src/channels/adapters/github/membership.ts +35 -0
- package/src/channels/adapters/github/outbound.ts +145 -0
- package/src/channels/adapters/github/webhook-register.ts +349 -0
- package/src/channels/manager.ts +94 -9
- package/src/channels/router.ts +28 -2
- package/src/channels/schema.ts +31 -1
- package/src/channels/tunnel-bridge.ts +51 -0
- package/src/cli/builtins.ts +28 -0
- package/src/cli/channel.ts +511 -25
- package/src/cli/container-command-client.ts +244 -0
- package/src/cli/cron.ts +173 -0
- package/src/cli/host-command-runner.ts +150 -0
- package/src/cli/index.ts +42 -1
- package/src/cli/init.ts +256 -27
- package/src/cli/model.ts +4 -2
- package/src/cli/plugin-command-help.ts +49 -0
- package/src/cli/plugin-commands-dispatch.ts +112 -0
- package/src/cli/plugin-commands.ts +118 -0
- package/src/cli/tui.ts +10 -2
- package/src/cli/tunnel.ts +533 -0
- package/src/cli/ui.ts +8 -3
- package/src/cli/usage.ts +30 -2
- package/src/config/config.ts +90 -4
- package/src/config/reloadable.ts +22 -4
- package/src/container/start.ts +30 -3
- package/src/cron/bridge.ts +136 -0
- package/src/cron/consumer.ts +62 -6
- package/src/cron/index.ts +19 -2
- package/src/cron/list.ts +105 -0
- package/src/cron/scheduler.ts +12 -3
- package/src/cron/schema.ts +11 -3
- package/src/doctor/checks.ts +0 -50
- package/src/init/dockerfile.ts +59 -13
- package/src/init/ensure-deps.ts +15 -4
- package/src/init/github-webhook-install.ts +109 -0
- package/src/init/index.ts +505 -9
- package/src/init/run-bun-install.ts +17 -3
- package/src/init/run-owner-claim.ts +11 -2
- package/src/permissions/builtins.ts +6 -1
- package/src/permissions/match-rule.ts +24 -2
- package/src/permissions/resolve.ts +1 -0
- package/src/plugin/define.ts +42 -1
- package/src/plugin/index.ts +18 -3
- package/src/plugin/manager.ts +2 -0
- package/src/plugin/registry.ts +85 -3
- package/src/plugin/types.ts +138 -1
- package/src/plugin/zod-introspect.ts +100 -0
- package/src/role-claim/match-rule.ts +2 -1
- package/src/run/index.ts +119 -4
- package/src/secrets/index.ts +1 -1
- package/src/secrets/schema.ts +21 -0
- package/src/server/command-runner.ts +476 -0
- package/src/server/index.ts +393 -15
- package/src/shared/index.ts +8 -0
- package/src/shared/protocol.ts +80 -1
- package/src/skills/typeclaw-channel-github/SKILL.md +24 -0
- package/src/skills/typeclaw-config/SKILL.md +27 -26
- package/src/skills/typeclaw-cron/SKILL.md +234 -3
- package/src/skills/typeclaw-monorepo/SKILL.md +2 -2
- package/src/skills/typeclaw-permissions/SKILL.md +5 -4
- package/src/skills/typeclaw-plugins/SKILL.md +251 -5
- package/src/skills/typeclaw-tunnels/SKILL.md +111 -0
- package/src/test-helpers/wait-for.ts +50 -0
- package/src/tui/index.ts +35 -4
- package/src/tunnels/__fixtures__/cloudflared-quick-stderr.txt +11 -0
- package/src/tunnels/events.ts +14 -0
- package/src/tunnels/index.ts +12 -0
- package/src/tunnels/log-ring.ts +54 -0
- package/src/tunnels/manager.ts +139 -0
- package/src/tunnels/providers/cloudflare-quick.ts +189 -0
- package/src/tunnels/providers/external.ts +53 -0
- package/src/tunnels/quick-url-parser.ts +5 -0
- package/src/tunnels/types.ts +43 -0
- package/src/usage/aggregate.ts +30 -1
- package/src/usage/index.ts +3 -2
- package/src/usage/report.ts +103 -3
- package/src/usage/scan.ts +59 -4
- package/typeclaw.schema.json +254 -1
package/secrets.schema.json
CHANGED
|
@@ -142,6 +142,119 @@
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
},
|
|
145
|
+
"github": {
|
|
146
|
+
"type": "object",
|
|
147
|
+
"properties": {
|
|
148
|
+
"auth": {
|
|
149
|
+
"oneOf": [
|
|
150
|
+
{
|
|
151
|
+
"type": "object",
|
|
152
|
+
"properties": {
|
|
153
|
+
"type": {
|
|
154
|
+
"type": "string",
|
|
155
|
+
"const": "pat"
|
|
156
|
+
},
|
|
157
|
+
"token": {
|
|
158
|
+
"anyOf": [
|
|
159
|
+
{
|
|
160
|
+
"type": "string",
|
|
161
|
+
"minLength": 1
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"type": "object",
|
|
165
|
+
"properties": {
|
|
166
|
+
"value": {
|
|
167
|
+
"type": "string",
|
|
168
|
+
"minLength": 1
|
|
169
|
+
},
|
|
170
|
+
"env": {
|
|
171
|
+
"type": "string",
|
|
172
|
+
"minLength": 1
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
"required": [
|
|
180
|
+
"type",
|
|
181
|
+
"token"
|
|
182
|
+
]
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"type": "object",
|
|
186
|
+
"properties": {
|
|
187
|
+
"type": {
|
|
188
|
+
"type": "string",
|
|
189
|
+
"const": "app"
|
|
190
|
+
},
|
|
191
|
+
"appId": {
|
|
192
|
+
"type": "integer",
|
|
193
|
+
"exclusiveMinimum": 0,
|
|
194
|
+
"maximum": 9007199254740991
|
|
195
|
+
},
|
|
196
|
+
"privateKey": {
|
|
197
|
+
"anyOf": [
|
|
198
|
+
{
|
|
199
|
+
"type": "string",
|
|
200
|
+
"minLength": 1
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"type": "object",
|
|
204
|
+
"properties": {
|
|
205
|
+
"value": {
|
|
206
|
+
"type": "string",
|
|
207
|
+
"minLength": 1
|
|
208
|
+
},
|
|
209
|
+
"env": {
|
|
210
|
+
"type": "string",
|
|
211
|
+
"minLength": 1
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
},
|
|
217
|
+
"installationId": {
|
|
218
|
+
"type": "integer",
|
|
219
|
+
"exclusiveMinimum": 0,
|
|
220
|
+
"maximum": 9007199254740991
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
"required": [
|
|
224
|
+
"type",
|
|
225
|
+
"appId",
|
|
226
|
+
"privateKey"
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
},
|
|
231
|
+
"webhookSecret": {
|
|
232
|
+
"anyOf": [
|
|
233
|
+
{
|
|
234
|
+
"type": "string",
|
|
235
|
+
"minLength": 1
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"type": "object",
|
|
239
|
+
"properties": {
|
|
240
|
+
"value": {
|
|
241
|
+
"type": "string",
|
|
242
|
+
"minLength": 1
|
|
243
|
+
},
|
|
244
|
+
"env": {
|
|
245
|
+
"type": "string",
|
|
246
|
+
"minLength": 1
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
]
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
"required": [
|
|
254
|
+
"auth",
|
|
255
|
+
"webhookSecret"
|
|
256
|
+
]
|
|
257
|
+
},
|
|
145
258
|
"telegram-bot": {
|
|
146
259
|
"type": "object",
|
|
147
260
|
"properties": {
|
package/src/agent/index.ts
CHANGED
|
@@ -29,8 +29,9 @@ import { lookAtTool } from './multimodal'
|
|
|
29
29
|
import { resolveBuiltinToolRefs, wrapPluginTool, wrapSystemAgentTool, wrapSystemTool } from './plugin-tools'
|
|
30
30
|
import { createReloadTool } from './reload-tool'
|
|
31
31
|
import { loadSelf } from './self'
|
|
32
|
+
import { SESSION_META_CUSTOM_TYPE, sessionMetaPayload } from './session-meta'
|
|
32
33
|
import { renderSessionOrigin, type SessionOrigin, type SessionRoleContext } from './session-origin'
|
|
33
|
-
import { DEFAULT_SYSTEM_PROMPT, renderRuntimeBlock } from './system-prompt'
|
|
34
|
+
import { DEFAULT_SYSTEM_PROMPT, renderRuntimeBlock, SLIM_SYSTEM_PROMPT } from './system-prompt'
|
|
34
35
|
import {
|
|
35
36
|
createBudgetState,
|
|
36
37
|
type ToolResultBudget,
|
|
@@ -231,6 +232,25 @@ export async function createSessionWithDispose(options: CreateSessionOptions = {
|
|
|
231
232
|
// container-restarting broadcast.
|
|
232
233
|
const sessionManager = options.sessionManager ?? SessionManager.inMemory()
|
|
233
234
|
|
|
235
|
+
// Stamp a one-shot custom entry naming the session's origin kind so
|
|
236
|
+
// `typeclaw usage` can bucket tokens by tui/cron/channel/subagent. Pi's
|
|
237
|
+
// `appendCustomEntry` is the blessed extension point: the entry persists
|
|
238
|
+
// into the session JSONL alongside messages, does NOT participate in LLM
|
|
239
|
+
// context, and pi handles file-creation timing — the entry lands after the
|
|
240
|
+
// session header on first flush, so `SessionManager.open()` keeps reading
|
|
241
|
+
// a canonical session file. Skipped for reopened sessions (a prior stamp
|
|
242
|
+
// is already in `getEntries()`) so usage attribution stays stable across
|
|
243
|
+
// restarts. Also skipped when origin is unknown (inMemory subagents) or
|
|
244
|
+
// when the manager is not persisted.
|
|
245
|
+
if (options.origin !== undefined && sessionManager.getSessionFile() !== undefined) {
|
|
246
|
+
const alreadyStamped = sessionManager
|
|
247
|
+
.getEntries()
|
|
248
|
+
.some((e) => e.type === 'custom' && e.customType === SESSION_META_CUSTOM_TYPE)
|
|
249
|
+
if (!alreadyStamped) {
|
|
250
|
+
sessionManager.appendCustomEntry(SESSION_META_CUSTOM_TYPE, sessionMetaPayload(options.origin))
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
234
254
|
const customSystemTools =
|
|
235
255
|
options.customTools !== undefined
|
|
236
256
|
? options.customTools
|
|
@@ -508,48 +528,147 @@ export type CreateResourceLoaderOptions = {
|
|
|
508
528
|
origin?: SessionOrigin
|
|
509
529
|
permissions?: PermissionService
|
|
510
530
|
runtimeVersion?: string
|
|
531
|
+
// Explicit override for the prompt mode. When omitted, the mode is derived
|
|
532
|
+
// from `origin.kind`: cron + subagent → slim, tui + channel → full. Pass
|
|
533
|
+
// 'full' to force the heavy prompt even on an unattended origin (rarely
|
|
534
|
+
// useful; mostly an escape hatch for ad-hoc debugging).
|
|
535
|
+
mode?: SystemPromptMode
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Origins where the operator-facing DEFAULT_SYSTEM_PROMPT, git-nudge, and the
|
|
539
|
+
// agent-folder commit guidance carry their weight: there is a human reading
|
|
540
|
+
// the output, the agent is expected to maintain its folder over time, and
|
|
541
|
+
// conversational register matters. For everything else (cron fires, default
|
|
542
|
+
// subagents), the slim prompt is the right default — the origin block already
|
|
543
|
+
// names the unattended context and tells the agent what's expected of it.
|
|
544
|
+
//
|
|
545
|
+
// Exhaustive switch (not a boolean expression) so a future origin kind forces
|
|
546
|
+
// the author to make an explicit full-or-slim decision at compile time. The
|
|
547
|
+
// previous form silently defaulted new origins to slim, which would have
|
|
548
|
+
// stripped the operator-facing prompt from a new interactive surface by
|
|
549
|
+
// accident.
|
|
550
|
+
export function deriveSystemPromptMode(origin: SessionOrigin | undefined): SystemPromptMode {
|
|
551
|
+
if (origin === undefined) return 'full'
|
|
552
|
+
switch (origin.kind) {
|
|
553
|
+
case 'tui':
|
|
554
|
+
case 'channel':
|
|
555
|
+
return 'full'
|
|
556
|
+
case 'cron':
|
|
557
|
+
case 'subagent':
|
|
558
|
+
return 'slim'
|
|
559
|
+
default: {
|
|
560
|
+
const _exhaustive: never = origin
|
|
561
|
+
void _exhaustive
|
|
562
|
+
return 'full'
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Pure inputs for `composeSystemPrompt`. Each field maps 1:1 to a rendered
|
|
568
|
+
// section of the prompt; callers that don't want a section pass `undefined`
|
|
569
|
+
// (or `''` for `gitNudge`). Extracted so the debug dumper in
|
|
570
|
+
// `scripts/dump-system-prompt.ts` can reuse the exact same composition
|
|
571
|
+
// pipeline `createResourceLoader` uses, with no risk of drift if the
|
|
572
|
+
// section order changes.
|
|
573
|
+
//
|
|
574
|
+
// `mode` selects the base prompt:
|
|
575
|
+
// - 'full' (default) — DEFAULT_SYSTEM_PROMPT (~2155 tok of operator-facing
|
|
576
|
+
// guidance: agent folder layout, version-control rules, register matching,
|
|
577
|
+
// workspace boundary). Right choice for TUI and channel sessions where a
|
|
578
|
+
// human is reading the output and the agent maintains its folder.
|
|
579
|
+
// - 'slim' — SLIM_SYSTEM_PROMPT (~80 tok). Right choice for cron jobs and
|
|
580
|
+
// default subagents — unattended sessions where most of the operator
|
|
581
|
+
// guidance is irrelevant and the origin block already covers per-kind
|
|
582
|
+
// specifics (no human, side effects via tools, narrow scope).
|
|
583
|
+
export type SystemPromptMode = 'full' | 'slim'
|
|
584
|
+
|
|
585
|
+
export type SystemPromptComposition = {
|
|
586
|
+
mode?: SystemPromptMode
|
|
587
|
+
self: string
|
|
588
|
+
runtimeVersion?: string
|
|
589
|
+
origin?: SessionOrigin
|
|
590
|
+
roleContext?: SessionRoleContext
|
|
591
|
+
gitNudge: string
|
|
592
|
+
memorySection: string
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Section-order contract for the system prompt. Kept as a pure string→string
|
|
596
|
+
// transform so it can be exercised without disk, plugin runtime, or auth.
|
|
597
|
+
//
|
|
598
|
+
// Cache-suffix ordering: least-volatile sections first, most-volatile last.
|
|
599
|
+
// This minimises the number of cached prompt bytes invalidated when a
|
|
600
|
+
// section changes (the provider's prompt cache hits up to the first byte
|
|
601
|
+
// that differs).
|
|
602
|
+
//
|
|
603
|
+
// 0. runtime block — most stable: only changes on typeclaw releases (rare).
|
|
604
|
+
// 1. origin block — stable across all sessions of the same kind.
|
|
605
|
+
// 2. gitNudge — rare changes; agent folders force-commit sessions/ and
|
|
606
|
+
// memory/ after every turn, so the dirty-files list is empty most of
|
|
607
|
+
// the time.
|
|
608
|
+
// 3. memorySection — most volatile: MEMORY.md grows on every dream cycle
|
|
609
|
+
// and memory/yyyy-MM-dd.md grows after every channel turn that triggers
|
|
610
|
+
// memory-logger. Pinning it to the end keeps everything above it
|
|
611
|
+
// cacheable across session resurrections.
|
|
612
|
+
export function composeSystemPrompt(parts: SystemPromptComposition): string {
|
|
613
|
+
const base = parts.mode === 'slim' ? SLIM_SYSTEM_PROMPT : DEFAULT_SYSTEM_PROMPT
|
|
614
|
+
let prompt = `${base}\n\n${parts.self}`
|
|
615
|
+
if (parts.runtimeVersion !== undefined) {
|
|
616
|
+
prompt = `${prompt}\n\n${renderRuntimeBlock(parts.runtimeVersion)}`
|
|
617
|
+
}
|
|
618
|
+
if (parts.origin !== undefined) {
|
|
619
|
+
prompt = `${prompt}\n\n${renderSessionOrigin(parts.origin, Date.now(), parts.roleContext)}`
|
|
620
|
+
}
|
|
621
|
+
if (parts.gitNudge !== '') {
|
|
622
|
+
prompt = `${prompt}\n\n${parts.gitNudge}`
|
|
623
|
+
}
|
|
624
|
+
if (parts.memorySection !== '') {
|
|
625
|
+
prompt = `${prompt}\n\n${parts.memorySection}`
|
|
626
|
+
}
|
|
627
|
+
return prompt
|
|
511
628
|
}
|
|
512
629
|
|
|
513
630
|
export async function createResourceLoader(options: CreateResourceLoaderOptions = {}): Promise<DefaultResourceLoader> {
|
|
514
631
|
const agentDir = options.agentDir ?? process.cwd()
|
|
515
|
-
const
|
|
516
|
-
|
|
632
|
+
const mode: SystemPromptMode = options.mode ?? deriveSystemPromptMode(options.origin)
|
|
633
|
+
const basePrompt = mode === 'slim' ? SLIM_SYSTEM_PROMPT : DEFAULT_SYSTEM_PROMPT
|
|
634
|
+
let self = await loadSelf(agentDir)
|
|
517
635
|
|
|
518
636
|
if (options.plugins) {
|
|
519
|
-
|
|
637
|
+
// The plugin hook receives the partially-assembled prompt (base + identity)
|
|
638
|
+
// so plugins can rewrite either section before the cache-suffix blocks are
|
|
639
|
+
// appended. The base reflects the resolved mode, so a slim cron session's
|
|
640
|
+
// plugin hook sees the slim base — plugins that read the base text get
|
|
641
|
+
// the same shape the agent will see.
|
|
642
|
+
const preHook = `${basePrompt}\n\n${self}`
|
|
643
|
+
const event = { prompt: preHook, sessionId: options.plugins.sessionId, agentDir, origin: options.origin }
|
|
520
644
|
await options.plugins.hooks.runSessionPrompt(event)
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
// This minimises the number of cached prompt bytes invalidated when a
|
|
526
|
-
// section changes (the provider's prompt cache hits up to the first byte
|
|
527
|
-
// that differs).
|
|
528
|
-
//
|
|
529
|
-
// 0. runtime block — most stable: only changes on typeclaw releases (rare).
|
|
530
|
-
// 1. origin block — stable across all sessions of the same kind.
|
|
531
|
-
// 2. gitNudge — rare changes; agent folders force-commit sessions/ and
|
|
532
|
-
// memory/ after every turn, so the dirty-files list is empty most of
|
|
533
|
-
// the time.
|
|
534
|
-
// 3. memorySection — most volatile: MEMORY.md grows on every dream cycle
|
|
535
|
-
// and memory/yyyy-MM-dd.md grows after every channel turn that triggers
|
|
536
|
-
// memory-logger. Pinning it to the end keeps everything above it
|
|
537
|
-
// cacheable across session resurrections.
|
|
538
|
-
if (options.runtimeVersion !== undefined) {
|
|
539
|
-
systemPrompt = `${systemPrompt}\n\n${renderRuntimeBlock(options.runtimeVersion)}`
|
|
540
|
-
}
|
|
541
|
-
systemPrompt = withOrigin(systemPrompt, options.origin, options.permissions)
|
|
542
|
-
|
|
543
|
-
const gitNudge = await renderGitNudge(agentDir)
|
|
544
|
-
if (gitNudge !== '') {
|
|
545
|
-
systemPrompt = `${systemPrompt}\n\n${gitNudge}`
|
|
645
|
+
// Recover `self` by stripping the leading base so the rest of the
|
|
646
|
+
// composition stays section-shaped. If a plugin rewrote the base prompt as
|
|
647
|
+
// well, the recovered `self` carries the full mutated remainder.
|
|
648
|
+
self = event.prompt.startsWith(`${basePrompt}\n\n`) ? event.prompt.slice(basePrompt.length + 2) : event.prompt
|
|
546
649
|
}
|
|
547
650
|
|
|
651
|
+
const roleContext = options.origin !== undefined ? resolveRoleContext(options.origin, options.permissions) : undefined
|
|
652
|
+
// Slim mode skips git-nudge entirely: cron + subagent sessions are not the
|
|
653
|
+
// right actor to drive interactive commit decisions, and the operator-facing
|
|
654
|
+
// commit guidance the nudge points back to is itself excluded from the slim
|
|
655
|
+
// base prompt. Memory is still included so cron jobs that depend on MEMORY.md
|
|
656
|
+
// context (e.g. "send today's standup summary") keep working.
|
|
657
|
+
const gitNudge = mode === 'slim' ? '' : await renderGitNudge(agentDir)
|
|
548
658
|
const memorySection = await loadMemory(agentDir, {
|
|
549
659
|
...(options.origin !== undefined ? { origin: options.origin } : {}),
|
|
550
660
|
...(options.plugins?.sessionId !== undefined ? { currentSessionId: options.plugins.sessionId } : {}),
|
|
551
661
|
})
|
|
552
|
-
|
|
662
|
+
|
|
663
|
+
const systemPrompt = composeSystemPrompt({
|
|
664
|
+
mode,
|
|
665
|
+
self,
|
|
666
|
+
...(options.runtimeVersion !== undefined ? { runtimeVersion: options.runtimeVersion } : {}),
|
|
667
|
+
...(options.origin !== undefined ? { origin: options.origin } : {}),
|
|
668
|
+
...(roleContext !== undefined ? { roleContext } : {}),
|
|
669
|
+
gitNudge,
|
|
670
|
+
memorySection,
|
|
671
|
+
})
|
|
553
672
|
|
|
554
673
|
const additionalSkillPaths = [getBundledSkillsDir()]
|
|
555
674
|
// pi-coding-agent's DefaultResourceLoader auto-discovers <agentDir>/skills/
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AgentSession } from './index'
|
|
2
|
+
|
|
3
|
+
// pi-coding-agent encodes upstream LLM failures (billing, rate limit, network,
|
|
4
|
+
// malformed response, etc.) in the assistant message itself rather than
|
|
5
|
+
// throwing — `stopReason: 'error'` with a populated `errorMessage`. Code that
|
|
6
|
+
// only catches throws around `session.prompt()` therefore never sees these:
|
|
7
|
+
// the prompt resolves normally, no text deltas were emitted, and the only
|
|
8
|
+
// signal is the final `message_end` event. Channels, cron, and subagents all
|
|
9
|
+
// have to subscribe to surface these soft errors.
|
|
10
|
+
//
|
|
11
|
+
// Hard throws (timeouts, network drops, etc.) come out of the upstream wrapper
|
|
12
|
+
// as exceptions and are handled by the surrounding try/catch in each caller —
|
|
13
|
+
// not by this helper.
|
|
14
|
+
|
|
15
|
+
export type DetectedProviderError = {
|
|
16
|
+
message: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function detectProviderError(message: unknown): DetectedProviderError | null {
|
|
20
|
+
if (typeof message !== 'object' || message === null) return null
|
|
21
|
+
const m = message as { role?: unknown; stopReason?: unknown; errorMessage?: unknown }
|
|
22
|
+
if (m.role !== 'assistant') return null
|
|
23
|
+
// 'aborted' is fired when the user hits Escape — not a provider failure,
|
|
24
|
+
// and the TUI shows its own abort feedback elsewhere. Channels/cron just
|
|
25
|
+
// ignore aborts (no surface to render them on).
|
|
26
|
+
if (m.stopReason !== 'error') return null
|
|
27
|
+
const text = typeof m.errorMessage === 'string' && m.errorMessage.length > 0 ? m.errorMessage : 'LLM call failed'
|
|
28
|
+
return { message: text }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ProviderErrorListener = (error: DetectedProviderError) => void
|
|
32
|
+
export type Unsubscribe = () => void
|
|
33
|
+
|
|
34
|
+
// Subscribes to `message_end` events on `session` and invokes `onError` once
|
|
35
|
+
// per detected provider error. Returns the unsubscribe handle from the
|
|
36
|
+
// underlying `session.subscribe`. Callers MUST unsubscribe when the session
|
|
37
|
+
// is disposed to avoid leaks across sessions.
|
|
38
|
+
export function subscribeProviderErrors(session: AgentSession, onError: ProviderErrorListener): Unsubscribe {
|
|
39
|
+
return session.subscribe((event) => {
|
|
40
|
+
if (event.type !== 'message_end') return
|
|
41
|
+
const detected = detectProviderError(event.message)
|
|
42
|
+
if (detected !== null) onError(detected)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { SessionOrigin } from './session-origin'
|
|
2
|
+
|
|
3
|
+
export const SESSION_META_CUSTOM_TYPE = 'typeclaw.session-meta'
|
|
4
|
+
|
|
5
|
+
export type SessionMetaPayload = {
|
|
6
|
+
origin: MinimalSessionOrigin
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type MinimalSessionOrigin =
|
|
10
|
+
| { kind: 'tui' }
|
|
11
|
+
| { kind: 'cron'; jobId: string; jobKind: 'prompt' | 'exec' | 'subagent' | 'handler' }
|
|
12
|
+
| { kind: 'channel'; adapter: string; workspace: string; chat: string; thread: string | null }
|
|
13
|
+
| { kind: 'subagent'; subagent: string; parentSessionId: string }
|
|
14
|
+
|
|
15
|
+
// Reduce a full SessionOrigin to the minimum projection persisted to disk.
|
|
16
|
+
// Drops participant lists, membership counts, recursive provenance, and
|
|
17
|
+
// platform-rendered names — none of which `typeclaw usage` reads, and all of
|
|
18
|
+
// which would otherwise land in git history when sessions/ is auto-backed-up.
|
|
19
|
+
// Kept as a separate function so the boundary between "data the LLM sees in
|
|
20
|
+
// the system prompt" (full origin) and "data persisted for usage reporting"
|
|
21
|
+
// (this projection) stays explicit.
|
|
22
|
+
export function sessionMetaPayload(origin: SessionOrigin): SessionMetaPayload {
|
|
23
|
+
return { origin: minimalOrigin(origin) }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function minimalOrigin(origin: SessionOrigin): MinimalSessionOrigin {
|
|
27
|
+
switch (origin.kind) {
|
|
28
|
+
case 'tui':
|
|
29
|
+
return { kind: 'tui' }
|
|
30
|
+
case 'cron':
|
|
31
|
+
return { kind: 'cron', jobId: origin.jobId, jobKind: origin.jobKind }
|
|
32
|
+
case 'channel':
|
|
33
|
+
return {
|
|
34
|
+
kind: 'channel',
|
|
35
|
+
adapter: origin.adapter,
|
|
36
|
+
workspace: origin.workspace,
|
|
37
|
+
chat: origin.chat,
|
|
38
|
+
thread: origin.thread,
|
|
39
|
+
}
|
|
40
|
+
case 'subagent':
|
|
41
|
+
return { kind: 'subagent', subagent: origin.subagent, parentSessionId: origin.parentSessionId }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -25,7 +25,7 @@ export type SessionOrigin =
|
|
|
25
25
|
| {
|
|
26
26
|
kind: 'cron'
|
|
27
27
|
jobId: string
|
|
28
|
-
jobKind: 'prompt' | 'exec' | 'subagent'
|
|
28
|
+
jobKind: 'prompt' | 'exec' | 'subagent' | 'handler'
|
|
29
29
|
scheduledByRole?: string
|
|
30
30
|
scheduledByOrigin?: SessionOrigin | { kind: 'config-file' }
|
|
31
31
|
}
|
|
@@ -78,6 +78,7 @@ type PlatformInfo = {
|
|
|
78
78
|
const PLATFORM_INFO: Record<AdapterId, PlatformInfo> = {
|
|
79
79
|
'slack-bot': { displayName: 'Slack', mentionMode: 'angle-id' },
|
|
80
80
|
'discord-bot': { displayName: 'Discord', mentionMode: 'angle-id' },
|
|
81
|
+
github: { displayName: 'GitHub', mentionMode: 'at-username' },
|
|
81
82
|
'telegram-bot': { displayName: 'Telegram', mentionMode: 'at-username' },
|
|
82
83
|
kakaotalk: { displayName: 'KakaoTalk', mentionMode: 'alias' },
|
|
83
84
|
}
|
|
@@ -150,7 +151,7 @@ function renderTuiOrigin(): string {
|
|
|
150
151
|
].join('\n')
|
|
151
152
|
}
|
|
152
153
|
|
|
153
|
-
function renderCronOrigin(origin: { jobId: string; jobKind: 'prompt' | 'exec' | 'subagent' }): string {
|
|
154
|
+
function renderCronOrigin(origin: { jobId: string; jobKind: 'prompt' | 'exec' | 'subagent' | 'handler' }): string {
|
|
154
155
|
return [
|
|
155
156
|
'## Session origin',
|
|
156
157
|
'',
|
package/src/agent/subagents.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { HookBus } from '@/plugin'
|
|
|
5
5
|
import type { Stream, Unsubscribe } from '@/stream'
|
|
6
6
|
|
|
7
7
|
import { type AgentSession, createSession } from './index'
|
|
8
|
+
import { subscribeProviderErrors } from './provider-error'
|
|
8
9
|
import type { SessionOrigin } from './session-origin'
|
|
9
10
|
import type { ToolResultBudget } from './tool-result-budget'
|
|
10
11
|
|
|
@@ -134,6 +135,7 @@ export type InvokeSubagentOptions = {
|
|
|
134
135
|
parentSessionId?: string
|
|
135
136
|
spawnedByRole?: string
|
|
136
137
|
spawnedByOrigin?: SessionOrigin
|
|
138
|
+
onProviderError?: (errorMessage: string) => void
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
export async function invokeSubagent(name: string, options: InvokeSubagentOptions): Promise<void> {
|
|
@@ -153,6 +155,10 @@ export async function invokeSubagent(name: string, options: InvokeSubagentOption
|
|
|
153
155
|
const { session, dispose, hooks, sessionId, agentDir, origin, getTranscriptPath } = normalizeSubagentSession(
|
|
154
156
|
await createSessionForSubagent(subagent, sessionOptions),
|
|
155
157
|
)
|
|
158
|
+
const unsubProviderErrors =
|
|
159
|
+
options.onProviderError !== undefined
|
|
160
|
+
? subscribeProviderErrors(session, (err) => options.onProviderError!(err.message))
|
|
161
|
+
: null
|
|
156
162
|
const turnEvent =
|
|
157
163
|
hooks && sessionId !== undefined && agentDir !== undefined
|
|
158
164
|
? { sessionId, agentDir, ...(origin !== undefined ? { origin } : {}) }
|
|
@@ -177,6 +183,7 @@ export async function invokeSubagent(name: string, options: InvokeSubagentOption
|
|
|
177
183
|
})
|
|
178
184
|
}
|
|
179
185
|
} finally {
|
|
186
|
+
unsubProviderErrors?.()
|
|
180
187
|
if (hooks && sessionId !== undefined) {
|
|
181
188
|
await hooks.runSessionEnd({ sessionId, ...(origin !== undefined ? { origin } : {}) })
|
|
182
189
|
}
|
|
@@ -308,6 +315,7 @@ export function createSubagentConsumer({
|
|
|
308
315
|
agentDir,
|
|
309
316
|
userPrompt: '',
|
|
310
317
|
payload: msg.payload,
|
|
318
|
+
onProviderError: (message) => logger.error(`[subagent] ${key}: LLM call failed: ${message}`),
|
|
311
319
|
...(target.parentSessionId !== undefined ? { parentSessionId: target.parentSessionId } : {}),
|
|
312
320
|
...(target.spawnedByRole !== undefined ? { spawnedByRole: target.spawnedByRole } : {}),
|
|
313
321
|
...(spawnedByOrigin !== undefined ? { spawnedByOrigin } : {}),
|
|
@@ -1,67 +1,58 @@
|
|
|
1
1
|
export const DEFAULT_SYSTEM_PROMPT = `You are a general-purpose AI agent running inside TypeClaw.
|
|
2
2
|
|
|
3
|
-
TypeClaw is
|
|
4
|
-
|
|
5
|
-
Each agent lives in its own container with its own folder, mounted at the current working directory. The folder is yours — your home, your memory, your record of who you are. Read from it freely. Write to it deliberately.
|
|
3
|
+
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
4
|
|
|
7
5
|
## Your agent folder
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **USER.md** *(read on demand)* — what you know about the person you work with. Their name, preferences, context, working style, in-jokes. First impressions are written here during hatching; keep expanding it as you learn more. Read it when context about the user would change your response.
|
|
15
|
-
- **MEMORY.md** *(always injected below under \`# Memory\`, do not write)* — long-term memory. A notebook of things worth remembering across sessions: decisions made, lessons learned, context that should survive beyond one conversation. **Do not edit it directly** — MEMORY.md is consolidated by the runtime during *dreaming* (offline reflection over recent sessions and daily streams). If something is worth remembering, surface it in your reply or in \`memory/\` daily streams; dreaming will fold it in.
|
|
7
|
+
- **IDENTITY.md** *(always injected below)* — your role and function. Edit when responsibilities change.
|
|
8
|
+
- **SOUL.md** *(always injected below)* — your character, tone, voice. Edit rarely.
|
|
9
|
+
- **USER.md** *(read on demand)* — what you know about the user. Update as you learn.
|
|
10
|
+
- **AGENTS.md** *(read on demand)* — your operating manual. Read at the start of any non-trivial task and re-read whenever process is unclear.
|
|
11
|
+
- **MEMORY.md** *(always injected below, READ-ONLY)* — long-term memory, owned by the dreaming subagent. To capture something memorable, surface it in your reply or in \`memory/\` daily streams; never edit MEMORY.md directly.
|
|
16
12
|
|
|
17
|
-
|
|
13
|
+
If a task reveals durable guidance or identity/user context, update the owning file (IDENTITY / SOUL / USER / AGENTS) — never MEMORY.md.
|
|
18
14
|
|
|
19
15
|
## Your workspace
|
|
20
16
|
|
|
21
|
-
- **\`workspace/\`** —
|
|
22
|
-
- **\`sessions/\`** — transcripts of past conversations
|
|
23
|
-
- **\`memory/\`** *(undreamed daily streams
|
|
24
|
-
- **\`memory/skills/\`** —
|
|
25
|
-
- **\`.agents/skills/\`** —
|
|
17
|
+
- **\`workspace/\`** — your free-write zone for drafts, scratch work, generated artifacts. Do not create files at the agent-folder root unless the user explicitly asks.
|
|
18
|
+
- **\`sessions/\`** — transcripts of past conversations. Runtime-managed; don't write here.
|
|
19
|
+
- **\`memory/\`** *(undreamed daily streams injected below)* — dated streams written by the memory-logger between sessions. Runtime-owned.
|
|
20
|
+
- **\`memory/skills/\`** — muscle-memory skills written by the dreaming subagent. Auto-loaded; don't write here directly.
|
|
21
|
+
- **\`.agents/skills/\`** — user-installed skills.
|
|
26
22
|
|
|
27
23
|
## Configuration
|
|
28
24
|
|
|
29
|
-
- **\`typeclaw.json\`** —
|
|
30
|
-
- **\`.env\`** — secrets (API keys, tokens). Gitignored. Never echo
|
|
25
|
+
- **\`typeclaw.json\`** — runtime config. Read when needed.
|
|
26
|
+
- **\`.env\`** and **\`secrets.json\`** — secrets (API keys, tokens, OAuth credentials). Gitignored. Never echo, log, or commit these values.
|
|
31
27
|
|
|
32
28
|
## Execution bias
|
|
33
29
|
|
|
34
|
-
|
|
30
|
+
When the user gives you work, start doing it in the same turn — a real action, not a plan or a promise-to-act. Commentary-only turns are incomplete when the next action is clear. For multi-step work, send one short progress update, not a running narration.
|
|
35
31
|
|
|
36
32
|
## Tool-call style
|
|
37
33
|
|
|
38
|
-
Do not narrate routine, low-risk tool calls. Just call the tool. Narrate only when it helps: multi-step work, risky actions (deletions, external sends, irreversible changes), or when the user asks.
|
|
34
|
+
Do not narrate routine, low-risk tool calls. Just call the tool. Narrate only when it helps: multi-step work, risky actions (deletions, external sends, irreversible changes), or when the user asks.
|
|
39
35
|
|
|
40
36
|
## Version control
|
|
41
37
|
|
|
42
|
-
Your agent folder is a git repository
|
|
38
|
+
Your agent folder is a git repository.
|
|
43
39
|
|
|
44
|
-
-
|
|
45
|
-
- Use \`
|
|
46
|
-
-
|
|
47
|
-
- Never
|
|
48
|
-
- \`sessions/\` and \`memory/\` are also gitignored, but the runtime force-commits them on its own (auto-backup for sessions, dreaming for memory). Don't \`git add\` them, don't write commit messages about them, and don't be surprised when they appear in \`git log\`.
|
|
49
|
-
- If multiple unrelated changes piled up, split them into separate commits before declaring done. Clean history matters.
|
|
50
|
-
- Never \`git push\`, \`git reset --hard\`, \`git rebase\`, or rewrite remote history unless the user explicitly asks for it.
|
|
40
|
+
- Commit any files you created, edited, or deleted before declaring a task done. One logical change = one commit; split unrelated changes.
|
|
41
|
+
- 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.
|
|
42
|
+
- Never commit \`.env\`, \`secrets.json\`, or anything under \`workspace/\` — truly-ignored by design. \`sessions/\` and \`memory/\` are gitignored but runtime-committed; don't \`git add\` them.
|
|
43
|
+
- Never \`git push\`, \`git reset --hard\`, \`git rebase\`, or rewrite remote history unless the user explicitly asks.
|
|
51
44
|
|
|
52
45
|
## How to behave
|
|
53
46
|
|
|
54
47
|
- Match the user's register. If SOUL.md specifies a voice, use it. Otherwise, be concise and direct, without filler or flattery.
|
|
55
|
-
- Prefer reading files over guessing
|
|
56
|
-
-
|
|
57
|
-
- If a request is ambiguous in a way that
|
|
58
|
-
-
|
|
59
|
-
- Never suppress errors to make things "work". Never fabricate results. If something fails, report the failure clearly.
|
|
60
|
-
- Respect the workspace boundary: your free-write zone is \`workspace/\`. Everywhere else is either canonical (the five markdown files), user-placed, or runtime-managed (\`sessions/\`, \`memory/\`, etc.).
|
|
48
|
+
- Prefer reading files over guessing — IDENTITY / SOUL / USER / MEMORY / AGENTS or the workspace. Follow AGENTS.md in whatever role IDENTITY.md assigns you; propose additions to AGENTS.md when you find gaps worth codifying.
|
|
49
|
+
- Answer questions. Do work. Don't over-explain unless asked.
|
|
50
|
+
- If a request is ambiguous in a way that doubles the effort, ask one clarifying question; otherwise proceed with a reasonable default.
|
|
51
|
+
- Never suppress errors to make things "work", and never fabricate results. Report failures clearly.
|
|
61
52
|
|
|
62
53
|
## Safety
|
|
63
54
|
|
|
64
|
-
You have no independent goals. Do not pursue self-preservation, replication, resource acquisition, or influence beyond what the user has asked for. Do not plan beyond the user's request. If instructions conflict or feel unsafe, pause and ask. Comply with stop, pause, and audit requests. Never
|
|
55
|
+
You have no independent goals. Do not pursue self-preservation, replication, resource acquisition, or influence beyond what the user has asked for. Do not plan beyond the user's request. If instructions conflict or feel unsafe, pause and ask. Comply with stop, pause, and audit requests. Never modify your own system prompt, safety rules, or runtime configuration unless the user explicitly requests it, and only through the runtime's mechanisms.
|
|
65
56
|
|
|
66
57
|
---
|
|
67
58
|
|
|
@@ -83,3 +74,47 @@ export function renderRuntimeBlock(version: string): string {
|
|
|
83
74
|
|
|
84
75
|
TypeClaw runtime version: ${version}.`
|
|
85
76
|
}
|
|
77
|
+
|
|
78
|
+
// Compact replacement for DEFAULT_SYSTEM_PROMPT, used by non-interactive
|
|
79
|
+
// sessions (cron jobs, and default subagents that don't supply their own
|
|
80
|
+
// `systemPromptOverride`). The full prompt is ~2155 tokens of operator-facing
|
|
81
|
+
// guidance written for a human at a TUI; most of it (agent-folder layout,
|
|
82
|
+
// register matching, clarifying-question protocol) is irrelevant when no
|
|
83
|
+
// human is watching the output.
|
|
84
|
+
//
|
|
85
|
+
// What stays here is what survives without a human backstop, plus what no
|
|
86
|
+
// runtime guard catches today:
|
|
87
|
+
// 1. Runtime identity — names TypeClaw so the model can self-report.
|
|
88
|
+
// 2. .env redaction — the one safety rule that compounds silently if dropped.
|
|
89
|
+
// 3. Error/result honesty — the highest-risk drop. Unattended cron that
|
|
90
|
+
// fabricates success or swallows errors damages real state. The security
|
|
91
|
+
// plugin does not catch this.
|
|
92
|
+
// 4. Output discipline — keeps tool-call narration from bloating the
|
|
93
|
+
// ever-growing transcript that the next memory-logger pass has to read.
|
|
94
|
+
// 5. Filesystem hygiene — workspace boundary, MEMORY.md ownership, and
|
|
95
|
+
// runtime-managed paths (.env / sessions/ / memory/ / workspace/). The
|
|
96
|
+
// guard plugin blocks non-workspace writes for write/edit, but it
|
|
97
|
+
// explicitly allows MEMORY.md writes and does not gate bash/git on the
|
|
98
|
+
// runtime-managed paths.
|
|
99
|
+
//
|
|
100
|
+
// What does NOT live here, by design:
|
|
101
|
+
// - "No human is watching" / "produce side effects via channel_send" — both
|
|
102
|
+
// origin renderers (renderCronOrigin / renderSubagentOrigin) own this.
|
|
103
|
+
// - "Plain prose is invisible" — actively WRONG for subagents, whose plain
|
|
104
|
+
// text IS the deliverable to the parent session. The origin block tells
|
|
105
|
+
// each kind what its output channel is.
|
|
106
|
+
//
|
|
107
|
+
// The full DEFAULT_SYSTEM_PROMPT remains the right choice for TUI + channel
|
|
108
|
+
// sessions because there IS a human reading the output, the agent IS expected
|
|
109
|
+
// to maintain its agent folder over time, and conversational register matters.
|
|
110
|
+
export const SLIM_SYSTEM_PROMPT = `You are an AI agent running inside TypeClaw.
|
|
111
|
+
|
|
112
|
+
Never echo secrets from \`.env\` or \`secrets.json\`, or any credential you see in the environment. Never include them in tool calls, logs, or commit messages.
|
|
113
|
+
|
|
114
|
+
Never suppress errors to make things "work", and never fabricate results. If something fails, report the failure clearly so the next run or the operator can act on it.
|
|
115
|
+
|
|
116
|
+
Do not narrate routine, low-risk tool calls — just call the tool. Do not over-explain what you did unless asked.
|
|
117
|
+
|
|
118
|
+
Your free-write zone is \`workspace/\`. Do not create files at the root of the agent folder unless the prompt names another path. Do not edit \`MEMORY.md\` directly — the dreaming subagent owns it; to capture something memorable, surface it in your reply or in \`memory/\` daily streams. Never stage or commit \`.env\`, \`sessions/\`, \`memory/\`, or \`workspace/\` — those are runtime- or user-managed.
|
|
119
|
+
|
|
120
|
+
See the session-origin block below for what kind of session this is and what's expected of you.`
|