typeclaw 0.5.1 → 0.6.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 +4 -0
- package/package.json +1 -1
- package/src/agent/index.ts +80 -8
- package/src/agent/live-subagents.ts +215 -0
- package/src/agent/plugin-tools.ts +60 -20
- package/src/agent/session-origin.ts +15 -0
- package/src/agent/subagents.ts +140 -3
- package/src/agent/system-prompt.ts +40 -0
- package/src/agent/tools/channel-reply.ts +24 -1
- package/src/agent/tools/channel-send.ts +26 -1
- package/src/agent/tools/spawn-subagent.ts +283 -0
- package/src/agent/tools/subagent-cancel.ts +96 -0
- package/src/agent/tools/subagent-output.ts +192 -0
- package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +26 -0
- package/src/bundled-plugins/explorer/explorer.ts +103 -0
- package/src/bundled-plugins/explorer/index.ts +11 -0
- package/src/bundled-plugins/guard/index.ts +12 -1
- package/src/bundled-plugins/guard/policies/managed-config.ts +139 -0
- package/src/bundled-plugins/guard/policy.ts +1 -0
- package/src/bundled-plugins/operator/index.ts +11 -0
- package/src/bundled-plugins/operator/operator.ts +76 -0
- package/src/bundled-plugins/scout/index.ts +11 -0
- package/src/bundled-plugins/scout/scout.ts +94 -0
- package/src/channels/router.ts +32 -0
- package/src/config/config.ts +45 -12
- package/src/config/index.ts +3 -0
- package/src/cron/index.ts +3 -0
- package/src/cron/schema.ts +20 -0
- package/src/init/dockerfile.ts +44 -5
- package/src/permissions/builtins.ts +23 -2
- package/src/plugin/define.ts +2 -0
- package/src/plugin/index.ts +2 -0
- package/src/plugin/types.ts +15 -22
- package/src/run/bundled-plugins.ts +6 -0
- package/src/run/channel-session-factory.ts +19 -0
- package/src/run/index.ts +56 -6
- package/src/server/index.ts +103 -0
- package/src/skills/typeclaw-claude-code/SKILL.md +273 -0
- package/src/skills/typeclaw-claude-code/references/auth-flow.md +135 -0
- package/src/skills/typeclaw-claude-code/references/stop-hook.md +99 -0
- package/src/skills/typeclaw-claude-code/references/tmux-driving.md +157 -0
- package/src/skills/typeclaw-config/SKILL.md +29 -26
- package/typeclaw.schema.json +6 -0
package/README.md
CHANGED
|
@@ -140,6 +140,10 @@ bun run format
|
|
|
140
140
|
|
|
141
141
|
See [AGENTS.md](./AGENTS.md) for the long-form architecture notes — stages, hostd internals, message stream, plugin contracts, and the testing philosophy.
|
|
142
142
|
|
|
143
|
+
## Website
|
|
144
|
+
|
|
145
|
+
The landing page and documentation site at [typeclaw.dev](https://typeclaw.dev) lives in [`docs/`](./docs/). It's a Next.js + Fumadocs app — see [`docs/README.md`](./docs/README.md) for layout and the contributor workflow.
|
|
146
|
+
|
|
143
147
|
## License
|
|
144
148
|
|
|
145
149
|
MIT
|
package/package.json
CHANGED
package/src/agent/index.ts
CHANGED
|
@@ -25,12 +25,14 @@ import type { Stream } from '@/stream'
|
|
|
25
25
|
import { getAuthFor } from './auth'
|
|
26
26
|
import { createCompactionSettingsManager } from './compaction'
|
|
27
27
|
import { renderGitNudge } from './git-nudge'
|
|
28
|
+
import type { LiveSubagentRegistry } from './live-subagents'
|
|
28
29
|
import { lookAtTool } from './multimodal'
|
|
29
30
|
import { resolveBuiltinToolRefs, wrapPluginTool, wrapSystemAgentTool, wrapSystemTool } from './plugin-tools'
|
|
30
31
|
import { createReloadTool } from './reload-tool'
|
|
31
32
|
import { loadSelf } from './self'
|
|
32
33
|
import { SESSION_META_CUSTOM_TYPE, sessionMetaPayload } from './session-meta'
|
|
33
34
|
import { renderSessionOrigin, type SessionOrigin, type SessionRoleContext } from './session-origin'
|
|
35
|
+
import type { CreateSessionForSubagent, SubagentRegistry } from './subagents'
|
|
34
36
|
import { DEFAULT_SYSTEM_PROMPT, renderRuntimeBlock, SLIM_SYSTEM_PROMPT } from './system-prompt'
|
|
35
37
|
import {
|
|
36
38
|
createBudgetState,
|
|
@@ -43,7 +45,10 @@ import { createChannelHistoryTool } from './tools/channel-history'
|
|
|
43
45
|
import { createChannelReplyTool } from './tools/channel-reply'
|
|
44
46
|
import { createChannelSendTool } from './tools/channel-send'
|
|
45
47
|
import { createRestartTool } from './tools/restart'
|
|
48
|
+
import { createSpawnSubagentTool } from './tools/spawn-subagent'
|
|
46
49
|
import { createStreamSnapshotTool } from './tools/stream-snapshot'
|
|
50
|
+
import { createSubagentCancelTool } from './tools/subagent-cancel'
|
|
51
|
+
import { createSubagentOutputTool } from './tools/subagent-output'
|
|
47
52
|
import { webfetchTool } from './tools/webfetch'
|
|
48
53
|
import { websearchTool } from './tools/websearch'
|
|
49
54
|
|
|
@@ -153,6 +158,16 @@ export type CreateSessionOptions = {
|
|
|
153
158
|
// already seen") provide their own here. See `ToolResultBudget` for the
|
|
154
159
|
// shared shape.
|
|
155
160
|
toolResultBudgetMessage?: ToolResultBudget['exhaustedMessage']
|
|
161
|
+
// Orchestration wiring. When all three of `liveSubagentRegistry`,
|
|
162
|
+
// `subagentRegistry`, and `createSessionForSubagent` are present (AND
|
|
163
|
+
// `pluginSubagent` is unset), the session exposes the spawn_subagent,
|
|
164
|
+
// subagent_output, and subagent_cancel tools. Subagent-origin sessions
|
|
165
|
+
// get an empty tool set via the `pluginSubagent` branch; the gate here
|
|
166
|
+
// (omitting these for subagent sessions) is what prevents recursive
|
|
167
|
+
// spawning.
|
|
168
|
+
liveSubagentRegistry?: LiveSubagentRegistry
|
|
169
|
+
subagentRegistry?: SubagentRegistry
|
|
170
|
+
createSessionForSubagent?: CreateSessionForSubagent
|
|
156
171
|
}
|
|
157
172
|
|
|
158
173
|
export type CreateSessionResult = {
|
|
@@ -205,9 +220,16 @@ export async function createSessionWithDispose(options: CreateSessionOptions = {
|
|
|
205
220
|
const getOrigin: () => SessionOrigin | undefined =
|
|
206
221
|
options.originRef !== undefined ? () => options.originRef!.current : () => options.origin
|
|
207
222
|
|
|
208
|
-
|
|
223
|
+
// Subagent built-in tool refs are dual-routed (see BUILTIN_TOOL_DEFINITION
|
|
224
|
+
// dual-map in plugin-tools.ts): pi-side coding tools go to `tools:` so they
|
|
225
|
+
// become the strict base set, typeclaw-side web tools go to `customTools:`.
|
|
226
|
+
// The two `tools:` fields below (effective `options.tools` and the resolved
|
|
227
|
+
// subagent pi-side builtins) are mutually exclusive — `options.tools` is only
|
|
228
|
+
// passed by non-subagent callers like multimodal look-at; subagent sessions
|
|
229
|
+
// never set both.
|
|
230
|
+
const resolvedSubagentBuiltins = options.pluginSubagent?.toolRefs
|
|
209
231
|
? resolveBuiltinToolRefs(options.pluginSubagent.toolRefs)
|
|
210
|
-
:
|
|
232
|
+
: { agentTools: [], toolDefinitions: [] }
|
|
211
233
|
const pluginCustomTools = options.pluginSubagent
|
|
212
234
|
? wrapSubagentCustomTools(options.pluginSubagent, options.plugins, getOrigin)
|
|
213
235
|
: wrapRegistryTools(options.plugins, getOrigin)
|
|
@@ -224,11 +246,9 @@ export async function createSessionWithDispose(options: CreateSessionOptions = {
|
|
|
224
246
|
: undefined
|
|
225
247
|
const sessionBudgetState = sessionBudget ? createBudgetState() : undefined
|
|
226
248
|
|
|
227
|
-
const
|
|
228
|
-
options.tools ?? (
|
|
229
|
-
|
|
230
|
-
getOrigin,
|
|
231
|
-
)
|
|
249
|
+
const effectiveTools =
|
|
250
|
+
options.tools ?? (options.pluginSubagent ? (resolvedSubagentBuiltins.agentTools as AgentSessionTools) : undefined)
|
|
251
|
+
const hookWrappedTools = wrapSystemAgentTools(effectiveTools, options.plugins, getOrigin)
|
|
232
252
|
const tools =
|
|
233
253
|
sessionBudget && sessionBudgetState && hookWrappedTools
|
|
234
254
|
? (hookWrappedTools.map((t) =>
|
|
@@ -265,7 +285,7 @@ export async function createSessionWithDispose(options: CreateSessionOptions = {
|
|
|
265
285
|
options.customTools !== undefined
|
|
266
286
|
? options.customTools
|
|
267
287
|
: options.pluginSubagent
|
|
268
|
-
?
|
|
288
|
+
? resolvedSubagentBuiltins.toolDefinitions
|
|
269
289
|
: [
|
|
270
290
|
websearchTool,
|
|
271
291
|
webfetchTool,
|
|
@@ -282,6 +302,16 @@ export async function createSessionWithDispose(options: CreateSessionOptions = {
|
|
|
282
302
|
}),
|
|
283
303
|
]
|
|
284
304
|
: []),
|
|
305
|
+
...buildSubagentOrchestrationTools({
|
|
306
|
+
liveRegistry: options.liveSubagentRegistry,
|
|
307
|
+
registry: options.subagentRegistry,
|
|
308
|
+
createSessionForSubagent: options.createSessionForSubagent,
|
|
309
|
+
agentDir: options.plugins?.agentDir,
|
|
310
|
+
parentSessionId: sessionManager.getSessionId(),
|
|
311
|
+
getOrigin,
|
|
312
|
+
permissions: options.permissions,
|
|
313
|
+
stream: options.stream,
|
|
314
|
+
}),
|
|
285
315
|
]
|
|
286
316
|
const customToolsPreBudget = [...wrapSystemTools(customSystemTools, options.plugins, getOrigin), ...pluginCustomTools]
|
|
287
317
|
const customTools =
|
|
@@ -430,6 +460,48 @@ export function buildChannelTools(
|
|
|
430
460
|
return tools
|
|
431
461
|
}
|
|
432
462
|
|
|
463
|
+
export function buildSubagentOrchestrationTools(opts: {
|
|
464
|
+
liveRegistry: LiveSubagentRegistry | undefined
|
|
465
|
+
registry: SubagentRegistry | undefined
|
|
466
|
+
createSessionForSubagent: CreateSessionForSubagent | undefined
|
|
467
|
+
agentDir: string | undefined
|
|
468
|
+
parentSessionId: string
|
|
469
|
+
getOrigin: () => SessionOrigin | undefined
|
|
470
|
+
permissions: PermissionService | undefined
|
|
471
|
+
stream: Stream | undefined
|
|
472
|
+
}): ToolDefinition[] {
|
|
473
|
+
if (
|
|
474
|
+
opts.liveRegistry === undefined ||
|
|
475
|
+
opts.registry === undefined ||
|
|
476
|
+
opts.createSessionForSubagent === undefined ||
|
|
477
|
+
opts.agentDir === undefined
|
|
478
|
+
) {
|
|
479
|
+
return []
|
|
480
|
+
}
|
|
481
|
+
return [
|
|
482
|
+
createSpawnSubagentTool({
|
|
483
|
+
registry: opts.registry,
|
|
484
|
+
liveRegistry: opts.liveRegistry,
|
|
485
|
+
createSessionForSubagent: opts.createSessionForSubagent,
|
|
486
|
+
agentDir: opts.agentDir,
|
|
487
|
+
parentSessionId: opts.parentSessionId,
|
|
488
|
+
getOrigin: opts.getOrigin,
|
|
489
|
+
...(opts.permissions ? { permissions: opts.permissions } : {}),
|
|
490
|
+
...(opts.stream ? { stream: opts.stream } : {}),
|
|
491
|
+
}),
|
|
492
|
+
createSubagentOutputTool({
|
|
493
|
+
liveRegistry: opts.liveRegistry,
|
|
494
|
+
getOrigin: opts.getOrigin,
|
|
495
|
+
...(opts.permissions ? { permissions: opts.permissions } : {}),
|
|
496
|
+
}),
|
|
497
|
+
createSubagentCancelTool({
|
|
498
|
+
liveRegistry: opts.liveRegistry,
|
|
499
|
+
getOrigin: opts.getOrigin,
|
|
500
|
+
...(opts.permissions ? { permissions: opts.permissions } : {}),
|
|
501
|
+
}),
|
|
502
|
+
]
|
|
503
|
+
}
|
|
504
|
+
|
|
433
505
|
function wrapRegistryTools(
|
|
434
506
|
plugins: PluginSessionWiring | undefined,
|
|
435
507
|
getOrigin: () => SessionOrigin | undefined,
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { AgentSession } from './index'
|
|
2
|
+
|
|
3
|
+
export type SubagentProgressEvent =
|
|
4
|
+
| { kind: 'started'; ts: number }
|
|
5
|
+
| { kind: 'tool'; name: string; ok: boolean; ts: number }
|
|
6
|
+
| { kind: 'message'; preview: string; ts: number }
|
|
7
|
+
|
|
8
|
+
export type SubagentStatus = 'running' | 'completed' | 'failed'
|
|
9
|
+
|
|
10
|
+
export type SubagentCompletion = {
|
|
11
|
+
ok: boolean
|
|
12
|
+
finalMessage?: string
|
|
13
|
+
error?: string
|
|
14
|
+
durationMs: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type LiveSubagent = {
|
|
18
|
+
taskId: string
|
|
19
|
+
sessionId: string
|
|
20
|
+
subagentName: string
|
|
21
|
+
parentSessionId?: string
|
|
22
|
+
startedAt: number
|
|
23
|
+
status: SubagentStatus
|
|
24
|
+
completion?: SubagentCompletion
|
|
25
|
+
abort: () => Promise<void>
|
|
26
|
+
awaitCompletion: () => Promise<SubagentCompletion>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const MAX_EVENTS_PER_SUBAGENT = 100
|
|
30
|
+
export const MESSAGE_PREVIEW_CHARS = 200
|
|
31
|
+
|
|
32
|
+
type AgentSessionEvent =
|
|
33
|
+
| { type: 'message_update'; assistantMessageEvent: { type: string; delta?: string } }
|
|
34
|
+
| { type: 'message_end'; message: unknown }
|
|
35
|
+
| { type: 'tool_execution_start'; toolCallId: string; toolName: string; args: unknown }
|
|
36
|
+
| { type: 'tool_execution_end'; toolCallId: string; toolName: string; result: unknown; isError: boolean }
|
|
37
|
+
| { type: string }
|
|
38
|
+
|
|
39
|
+
export function coarsen(event: AgentSessionEvent, now: number): SubagentProgressEvent | null {
|
|
40
|
+
if (event.type === 'tool_execution_end') {
|
|
41
|
+
const ev = event as Extract<AgentSessionEvent, { type: 'tool_execution_end' }>
|
|
42
|
+
return { kind: 'tool', name: ev.toolName, ok: !ev.isError, ts: now }
|
|
43
|
+
}
|
|
44
|
+
if (event.type === 'message_end') {
|
|
45
|
+
const ev = event as Extract<AgentSessionEvent, { type: 'message_end' }>
|
|
46
|
+
const preview = extractMessagePreview(ev.message)
|
|
47
|
+
if (preview === null) return null
|
|
48
|
+
return { kind: 'message', preview, ts: now }
|
|
49
|
+
}
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function extractMessagePreview(message: unknown): string | null {
|
|
54
|
+
if (message === null || typeof message !== 'object') return null
|
|
55
|
+
const content = (message as { content?: unknown }).content
|
|
56
|
+
if (typeof content === 'string') {
|
|
57
|
+
const trimmed = content.trim()
|
|
58
|
+
return trimmed ? trimmed.slice(0, MESSAGE_PREVIEW_CHARS) : null
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(content)) {
|
|
61
|
+
for (const part of content) {
|
|
62
|
+
if (part && typeof part === 'object' && (part as { type?: unknown }).type === 'text') {
|
|
63
|
+
const text = (part as { text?: unknown }).text
|
|
64
|
+
if (typeof text === 'string') {
|
|
65
|
+
const trimmed = text.trim()
|
|
66
|
+
if (trimmed) return trimmed.slice(0, MESSAGE_PREVIEW_CHARS)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type StatusSnapshot = {
|
|
75
|
+
taskId: string
|
|
76
|
+
sessionId: string
|
|
77
|
+
subagentName: string
|
|
78
|
+
status: SubagentStatus
|
|
79
|
+
startedAt: number
|
|
80
|
+
elapsedMs: number
|
|
81
|
+
eventsCount: number
|
|
82
|
+
eventsRecent: SubagentProgressEvent[]
|
|
83
|
+
lastActivity: SubagentProgressEvent | null
|
|
84
|
+
statusSummary: string
|
|
85
|
+
completion?: SubagentCompletion
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class LiveSubagentRegistry {
|
|
89
|
+
private readonly entries = new Map<string, LiveSubagent>()
|
|
90
|
+
private readonly events = new Map<string, SubagentProgressEvent[]>()
|
|
91
|
+
|
|
92
|
+
register(live: LiveSubagent): void {
|
|
93
|
+
if (this.entries.has(live.taskId)) {
|
|
94
|
+
throw new Error(`task ${live.taskId} already registered`)
|
|
95
|
+
}
|
|
96
|
+
this.entries.set(live.taskId, live)
|
|
97
|
+
this.events.set(live.taskId, [{ kind: 'started', ts: live.startedAt }])
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
unregister(taskId: string): void {
|
|
101
|
+
this.entries.delete(taskId)
|
|
102
|
+
this.events.delete(taskId)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get(taskId: string): LiveSubagent | undefined {
|
|
106
|
+
return this.entries.get(taskId)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
list(filter?: { parentSessionId?: string }): LiveSubagent[] {
|
|
110
|
+
const all = Array.from(this.entries.values())
|
|
111
|
+
if (filter?.parentSessionId === undefined) return all
|
|
112
|
+
return all.filter((e) => e.parentSessionId === filter.parentSessionId)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
hasLiveForSession(sessionId: string): boolean {
|
|
116
|
+
for (const e of this.entries.values()) {
|
|
117
|
+
if (e.sessionId === sessionId && e.status === 'running') return true
|
|
118
|
+
}
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
recordEvent(taskId: string, event: SubagentProgressEvent): void {
|
|
123
|
+
const ring = this.events.get(taskId)
|
|
124
|
+
if (ring === undefined) return
|
|
125
|
+
ring.push(event)
|
|
126
|
+
if (ring.length > MAX_EVENTS_PER_SUBAGENT) {
|
|
127
|
+
ring.splice(0, ring.length - MAX_EVENTS_PER_SUBAGENT)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
recordCompletion(taskId: string, completion: SubagentCompletion): void {
|
|
132
|
+
const entry = this.entries.get(taskId)
|
|
133
|
+
if (entry === undefined) return
|
|
134
|
+
entry.completion = completion
|
|
135
|
+
entry.status = completion.ok ? 'completed' : 'failed'
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
snapshot(taskId: string, now: number = Date.now()): StatusSnapshot | undefined {
|
|
139
|
+
const entry = this.entries.get(taskId)
|
|
140
|
+
if (entry === undefined) return undefined
|
|
141
|
+
const events = this.events.get(taskId) ?? []
|
|
142
|
+
const eventsRecent = events.slice(-10)
|
|
143
|
+
const lastActivity: SubagentProgressEvent | null = events.length > 0 ? (events[events.length - 1] ?? null) : null
|
|
144
|
+
const elapsedMs = (entry.completion ? entry.startedAt + entry.completion.durationMs : now) - entry.startedAt
|
|
145
|
+
return {
|
|
146
|
+
taskId: entry.taskId,
|
|
147
|
+
sessionId: entry.sessionId,
|
|
148
|
+
subagentName: entry.subagentName,
|
|
149
|
+
status: entry.status,
|
|
150
|
+
startedAt: entry.startedAt,
|
|
151
|
+
elapsedMs,
|
|
152
|
+
eventsCount: events.length,
|
|
153
|
+
eventsRecent,
|
|
154
|
+
lastActivity,
|
|
155
|
+
statusSummary: renderStatusSummary(entry, events.length, lastActivity, elapsedMs),
|
|
156
|
+
...(entry.completion ? { completion: entry.completion } : {}),
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
clear(): void {
|
|
161
|
+
this.entries.clear()
|
|
162
|
+
this.events.clear()
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function renderStatusSummary(
|
|
167
|
+
entry: LiveSubagent,
|
|
168
|
+
eventsCount: number,
|
|
169
|
+
lastActivity: SubagentProgressEvent | null,
|
|
170
|
+
elapsedMs: number,
|
|
171
|
+
): string {
|
|
172
|
+
const elapsed = formatElapsed(elapsedMs)
|
|
173
|
+
if (entry.status === 'completed') return `Completed in ${elapsed}.`
|
|
174
|
+
if (entry.status === 'failed') {
|
|
175
|
+
const err = entry.completion?.error ?? 'unknown error'
|
|
176
|
+
return `Failed after ${elapsed}: ${err}`
|
|
177
|
+
}
|
|
178
|
+
const last = describeLastActivity(lastActivity)
|
|
179
|
+
return `Running for ${elapsed}. ${eventsCount} event${eventsCount === 1 ? '' : 's'} so far${last ? `. Last: ${last}` : ''}.`
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function describeLastActivity(event: SubagentProgressEvent | null): string | null {
|
|
183
|
+
if (event === null) return null
|
|
184
|
+
if (event.kind === 'tool') return `${event.ok ? '' : 'failed '}tool ${event.name}`
|
|
185
|
+
if (event.kind === 'message') {
|
|
186
|
+
const preview = event.preview.length > 60 ? `${event.preview.slice(0, 60)}…` : event.preview
|
|
187
|
+
return `message "${preview}"`
|
|
188
|
+
}
|
|
189
|
+
return null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function formatElapsed(ms: number): string {
|
|
193
|
+
if (ms < 1000) return `${ms}ms`
|
|
194
|
+
const totalSec = Math.floor(ms / 1000)
|
|
195
|
+
if (totalSec < 60) return `${totalSec}s`
|
|
196
|
+
const min = Math.floor(totalSec / 60)
|
|
197
|
+
const sec = totalSec % 60
|
|
198
|
+
return `${min}m${sec}s`
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function attachProgressCapture(
|
|
202
|
+
registry: LiveSubagentRegistry,
|
|
203
|
+
taskId: string,
|
|
204
|
+
session: Pick<AgentSession, 'subscribe'>,
|
|
205
|
+
): () => void {
|
|
206
|
+
const unsubscribe = session.subscribe((event: unknown) => {
|
|
207
|
+
const coarsened = coarsen(event as AgentSessionEvent, Date.now())
|
|
208
|
+
if (coarsened !== null) {
|
|
209
|
+
registry.recordEvent(taskId, coarsened)
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
return () => {
|
|
213
|
+
if (typeof unsubscribe === 'function') unsubscribe()
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -16,6 +16,7 @@ import { z } from 'zod'
|
|
|
16
16
|
|
|
17
17
|
import {
|
|
18
18
|
ACKNOWLEDGE_GUARDS,
|
|
19
|
+
checkManagedConfigGuard,
|
|
19
20
|
checkNonWorkspaceWriteGuard,
|
|
20
21
|
checkSkillAuthoringGuard,
|
|
21
22
|
} from '@/bundled-plugins/guard/policy'
|
|
@@ -31,15 +32,8 @@ import type {
|
|
|
31
32
|
} from '@/plugin'
|
|
32
33
|
|
|
33
34
|
import type { SessionOrigin } from './session-origin'
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
| typeof piReadTool
|
|
37
|
-
| typeof piBashTool
|
|
38
|
-
| typeof piEditTool
|
|
39
|
-
| typeof piWriteTool
|
|
40
|
-
| typeof piGrepTool
|
|
41
|
-
| typeof piFindTool
|
|
42
|
-
| typeof piLsTool
|
|
35
|
+
import { webfetchTool } from './tools/webfetch'
|
|
36
|
+
import { websearchTool } from './tools/websearch'
|
|
43
37
|
|
|
44
38
|
const ACKNOWLEDGE_GUARDS_SCHEMA = Type.Optional(
|
|
45
39
|
Type.Object(
|
|
@@ -50,22 +44,64 @@ const ACKNOWLEDGE_GUARDS_SCHEMA = Type.Optional(
|
|
|
50
44
|
),
|
|
51
45
|
)
|
|
52
46
|
|
|
53
|
-
|
|
47
|
+
// `BuiltinToolRef.__builtinTool` strings are dual-routed when a plugin
|
|
48
|
+
// subagent declares them: pi-coding-agent's own coding tools flow through
|
|
49
|
+
// `createAgentSession({ tools: AgentTool[] })` (which pi treats as a strict
|
|
50
|
+
// base-tool override — exactly the declared subset becomes active), and
|
|
51
|
+
// typeclaw's own web tools flow through `customTools: ToolDefinition[]` (the
|
|
52
|
+
// only path pi accepts for non-pi tool definitions). Routing typeclaw tools
|
|
53
|
+
// through `tools:` silently drops them (pi's `tools` validator rejects shapes
|
|
54
|
+
// it doesn't recognize); routing pi tools through `customTools:` would work
|
|
55
|
+
// but ALSO auto-injects pi's default 4 base tools (read/bash/edit/write),
|
|
56
|
+
// widening every plugin subagent's allowlist beyond what it declared. The
|
|
57
|
+
// dual route is the only shape that gives "subagent gets exactly what it
|
|
58
|
+
// asked for, nothing more." See `src/agent/index.ts` `createSessionWithDispose`
|
|
59
|
+
// for the consumer that splits the resolved arrays into the two pi fields.
|
|
60
|
+
type PiAgentToolName = 'read' | 'bash' | 'edit' | 'write' | 'grep' | 'find' | 'ls'
|
|
61
|
+
type TypeclawToolName = 'websearch' | 'webfetch'
|
|
62
|
+
|
|
63
|
+
const PI_AGENT_TOOL_MAP: Record<PiAgentToolName, AgentTool<any, any>> = {
|
|
64
|
+
read: piReadTool,
|
|
54
65
|
bash: piBashTool,
|
|
55
66
|
edit: piEditTool,
|
|
56
|
-
|
|
67
|
+
write: piWriteTool,
|
|
57
68
|
grep: piGrepTool,
|
|
69
|
+
find: piFindTool,
|
|
58
70
|
ls: piLsTool,
|
|
59
|
-
read: piReadTool,
|
|
60
|
-
write: piWriteTool,
|
|
61
71
|
}
|
|
62
72
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
const TYPECLAW_TOOL_DEFINITION_MAP: Record<TypeclawToolName, ToolDefinition<any, any, any>> = {
|
|
74
|
+
websearch: websearchTool,
|
|
75
|
+
webfetch: webfetchTool,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isPiAgentToolName(name: string): name is PiAgentToolName {
|
|
79
|
+
return name in PI_AGENT_TOOL_MAP
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isTypeclawToolName(name: string): name is TypeclawToolName {
|
|
83
|
+
return name in TYPECLAW_TOOL_DEFINITION_MAP
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type ResolvedBuiltinTools = {
|
|
87
|
+
agentTools: AgentTool<any, any>[]
|
|
88
|
+
toolDefinitions: ToolDefinition<any, any, any>[]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function resolveBuiltinToolRefs(refs: BuiltinToolRef[]): ResolvedBuiltinTools {
|
|
92
|
+
const agentTools: AgentTool<any, any>[] = []
|
|
93
|
+
const toolDefinitions: ToolDefinition<any, any, any>[] = []
|
|
94
|
+
for (const ref of refs) {
|
|
95
|
+
const name = ref.__builtinTool
|
|
96
|
+
if (isPiAgentToolName(name)) {
|
|
97
|
+
agentTools.push(PI_AGENT_TOOL_MAP[name])
|
|
98
|
+
} else if (isTypeclawToolName(name)) {
|
|
99
|
+
toolDefinitions.push(TYPECLAW_TOOL_DEFINITION_MAP[name])
|
|
100
|
+
} else {
|
|
101
|
+
throw new Error(`unknown built-in tool ref: ${name}`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { agentTools, toolDefinitions }
|
|
69
105
|
}
|
|
70
106
|
|
|
71
107
|
export type WrapToolOptions = {
|
|
@@ -274,7 +310,11 @@ function errorResult(message: string) {
|
|
|
274
310
|
}
|
|
275
311
|
|
|
276
312
|
async function runFinalWriteGuards(options: { tool: string; args: Record<string, unknown>; agentDir: string }) {
|
|
277
|
-
return (
|
|
313
|
+
return (
|
|
314
|
+
(await checkManagedConfigGuard(options)) ??
|
|
315
|
+
(await checkSkillAuthoringGuard(options)) ??
|
|
316
|
+
checkNonWorkspaceWriteGuard(options)
|
|
317
|
+
)
|
|
278
318
|
}
|
|
279
319
|
|
|
280
320
|
function withGuardAcknowledgements<TParams extends TSchema>(toolName: string, parameters: TParams): TParams {
|
|
@@ -226,6 +226,21 @@ function renderChannelOrigin(
|
|
|
226
226
|
'reply, your entire final visible response must be exactly `NO_REPLY`.',
|
|
227
227
|
'Any other visible text without a channel tool call is blocked.',
|
|
228
228
|
'',
|
|
229
|
+
'**Default to ONE reply per inbound.** Send a second `channel_reply` only',
|
|
230
|
+
'when the user genuinely benefits from it:',
|
|
231
|
+
'',
|
|
232
|
+
'- the user asked multiple distinct things and each deserves its own',
|
|
233
|
+
' scoped answer,',
|
|
234
|
+
'- your reply exceeds the platform message limit and must be chunked,',
|
|
235
|
+
'- you need to post an attachment AND commentary on it on Discord (on',
|
|
236
|
+
' Slack, pass `text` and `attachments` in a single `channel_reply` call),',
|
|
237
|
+
'- you are emitting progress updates during a long-running task and the',
|
|
238
|
+
' channel would otherwise sit silent.',
|
|
239
|
+
'',
|
|
240
|
+
'Do NOT send a second reply just to rephrase, restate, summarize, or',
|
|
241
|
+
'"confirm in plain language" something you already said. After the first',
|
|
242
|
+
'reply lands, end your turn — the user will respond if they want more.',
|
|
243
|
+
'',
|
|
229
244
|
'To reply in this conversation, call `channel_reply({ text })`. Addressing',
|
|
230
245
|
`is filled in from this session, including the thread${origin.thread !== null ? '' : ' (none here — this is a channel-root session)'}, so you don't`,
|
|
231
246
|
'need to copy any of these fields:',
|