typeclaw 0.1.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/LICENSE +21 -0
- package/README.md +134 -0
- package/auth.schema.json +63 -0
- package/cron.schema.json +96 -0
- package/package.json +72 -0
- package/scripts/emit-base-dockerfile.ts +5 -0
- package/scripts/generate-schema.ts +34 -0
- package/secrets.schema.json +63 -0
- package/src/agent/auth.ts +119 -0
- package/src/agent/compaction.ts +35 -0
- package/src/agent/git-nudge.ts +95 -0
- package/src/agent/index.ts +451 -0
- package/src/agent/plugin-tools.ts +269 -0
- package/src/agent/reload-tool.ts +71 -0
- package/src/agent/self.ts +45 -0
- package/src/agent/session-origin.ts +288 -0
- package/src/agent/subagents.ts +253 -0
- package/src/agent/system-prompt.ts +68 -0
- package/src/agent/tools/channel-fetch-attachment.ts +118 -0
- package/src/agent/tools/channel-history.ts +119 -0
- package/src/agent/tools/channel-reply.ts +182 -0
- package/src/agent/tools/channel-send.ts +212 -0
- package/src/agent/tools/ddg.ts +218 -0
- package/src/agent/tools/restart.ts +122 -0
- package/src/agent/tools/stream-snapshot.ts +181 -0
- package/src/agent/tools/webfetch/fetch.ts +102 -0
- package/src/agent/tools/webfetch/index.ts +1 -0
- package/src/agent/tools/webfetch/strategies/grep.ts +70 -0
- package/src/agent/tools/webfetch/strategies/jq.ts +31 -0
- package/src/agent/tools/webfetch/strategies/raw.ts +3 -0
- package/src/agent/tools/webfetch/strategies/readability.ts +30 -0
- package/src/agent/tools/webfetch/strategies/selector.ts +41 -0
- package/src/agent/tools/webfetch/strategies/snapshot.ts +135 -0
- package/src/agent/tools/webfetch/tool.ts +281 -0
- package/src/agent/tools/webfetch/types.ts +33 -0
- package/src/agent/tools/websearch.ts +96 -0
- package/src/agent/tools/wikipedia.ts +52 -0
- package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +170 -0
- package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +421 -0
- package/src/bundled-plugins/agent-browser/index.ts +179 -0
- package/src/bundled-plugins/agent-browser/shim-install.ts +158 -0
- package/src/bundled-plugins/agent-browser/shim.ts +152 -0
- package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +113 -0
- package/src/bundled-plugins/guard/index.ts +26 -0
- package/src/bundled-plugins/guard/policies/non-workspace-write.ts +98 -0
- package/src/bundled-plugins/guard/policies/skill-authoring.ts +185 -0
- package/src/bundled-plugins/guard/policies/uncommitted-changes.ts +85 -0
- package/src/bundled-plugins/guard/policy.ts +18 -0
- package/src/bundled-plugins/memory/README.md +71 -0
- package/src/bundled-plugins/memory/append-tool.ts +84 -0
- package/src/bundled-plugins/memory/dreaming-state.ts +86 -0
- package/src/bundled-plugins/memory/dreaming.ts +470 -0
- package/src/bundled-plugins/memory/fragment-parser.ts +67 -0
- package/src/bundled-plugins/memory/index.ts +238 -0
- package/src/bundled-plugins/memory/load-memory.ts +122 -0
- package/src/bundled-plugins/memory/memory-logger.ts +257 -0
- package/src/bundled-plugins/memory/secret-detector.ts +49 -0
- package/src/bundled-plugins/memory/watermark.ts +15 -0
- package/src/bundled-plugins/security/index.ts +35 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +120 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +167 -0
- package/src/bundled-plugins/security/policies/prompt-injection.ts +488 -0
- package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +99 -0
- package/src/bundled-plugins/security/policies/secret-exfil-read.ts +127 -0
- package/src/bundled-plugins/security/policies/session-search-secrets.ts +86 -0
- package/src/bundled-plugins/security/policies/ssrf.ts +196 -0
- package/src/bundled-plugins/security/policies/system-prompt-leak.ts +81 -0
- package/src/bundled-plugins/security/policy.ts +9 -0
- package/src/channels/adapters/discord-bot-channel-resolver.ts +77 -0
- package/src/channels/adapters/discord-bot-classify.ts +148 -0
- package/src/channels/adapters/discord-bot.ts +640 -0
- package/src/channels/adapters/kakaotalk-author-resolver.ts +78 -0
- package/src/channels/adapters/kakaotalk-channel-resolver.ts +105 -0
- package/src/channels/adapters/kakaotalk-classify.ts +77 -0
- package/src/channels/adapters/kakaotalk.ts +622 -0
- package/src/channels/adapters/slack-bot-author-resolver.ts +80 -0
- package/src/channels/adapters/slack-bot-channel-resolver.ts +84 -0
- package/src/channels/adapters/slack-bot-classify.ts +213 -0
- package/src/channels/adapters/slack-bot-dedupe.ts +51 -0
- package/src/channels/adapters/slack-bot-time.ts +10 -0
- package/src/channels/adapters/slack-bot.ts +881 -0
- package/src/channels/adapters/telegram-bot-classify.ts +155 -0
- package/src/channels/adapters/telegram-bot-format.ts +309 -0
- package/src/channels/adapters/telegram-bot.ts +604 -0
- package/src/channels/engagement.ts +227 -0
- package/src/channels/index.ts +21 -0
- package/src/channels/manager.ts +292 -0
- package/src/channels/membership-cache.ts +116 -0
- package/src/channels/membership-from-history.ts +53 -0
- package/src/channels/membership.ts +30 -0
- package/src/channels/participants.ts +47 -0
- package/src/channels/persistence.ts +209 -0
- package/src/channels/reloadable.ts +28 -0
- package/src/channels/router.ts +1570 -0
- package/src/channels/schema.ts +273 -0
- package/src/channels/types.ts +160 -0
- package/src/cli/channel.ts +403 -0
- package/src/cli/compose-status.ts +95 -0
- package/src/cli/compose.ts +240 -0
- package/src/cli/hostd.ts +163 -0
- package/src/cli/index.ts +27 -0
- package/src/cli/init.ts +592 -0
- package/src/cli/logs.ts +38 -0
- package/src/cli/reload.ts +68 -0
- package/src/cli/restart.ts +66 -0
- package/src/cli/run.ts +77 -0
- package/src/cli/shell.ts +33 -0
- package/src/cli/start.ts +57 -0
- package/src/cli/status.ts +178 -0
- package/src/cli/stop.ts +31 -0
- package/src/cli/tui.ts +35 -0
- package/src/cli/ui.ts +110 -0
- package/src/commands/index.ts +74 -0
- package/src/compose/discover.ts +43 -0
- package/src/compose/index.ts +25 -0
- package/src/compose/logs.ts +162 -0
- package/src/compose/restart.ts +69 -0
- package/src/compose/start.ts +62 -0
- package/src/compose/status.ts +28 -0
- package/src/compose/stop.ts +43 -0
- package/src/config/config.ts +424 -0
- package/src/config/index.ts +25 -0
- package/src/config/providers.ts +234 -0
- package/src/config/reloadable.ts +47 -0
- package/src/container/index.ts +27 -0
- package/src/container/logs.ts +37 -0
- package/src/container/port.ts +137 -0
- package/src/container/shared.ts +290 -0
- package/src/container/shell.ts +58 -0
- package/src/container/start.ts +670 -0
- package/src/container/status.ts +76 -0
- package/src/container/stop.ts +120 -0
- package/src/container/verify-running.ts +149 -0
- package/src/cron/consumer.ts +138 -0
- package/src/cron/index.ts +54 -0
- package/src/cron/reloadable.ts +64 -0
- package/src/cron/scheduler.ts +200 -0
- package/src/cron/schema.ts +96 -0
- package/src/hostd/client.ts +113 -0
- package/src/hostd/daemon.ts +587 -0
- package/src/hostd/index.ts +25 -0
- package/src/hostd/paths.ts +82 -0
- package/src/hostd/portbroker-manager.ts +101 -0
- package/src/hostd/protocol.ts +48 -0
- package/src/hostd/spawn.ts +224 -0
- package/src/hostd/supervisor.ts +60 -0
- package/src/hostd/tailscale.ts +172 -0
- package/src/hostd/version.ts +115 -0
- package/src/init/dockerfile.ts +327 -0
- package/src/init/ensure-deps.ts +152 -0
- package/src/init/gitignore.ts +46 -0
- package/src/init/hatching.ts +60 -0
- package/src/init/index.ts +786 -0
- package/src/init/kakaotalk-auth.ts +114 -0
- package/src/init/models-dev.ts +130 -0
- package/src/init/oauth-login.ts +74 -0
- package/src/init/packagejson.ts +94 -0
- package/src/init/paths.ts +2 -0
- package/src/init/run-bun-install.ts +20 -0
- package/src/markdown/chunk.ts +299 -0
- package/src/markdown/index.ts +1 -0
- package/src/plugin/context.ts +40 -0
- package/src/plugin/define.ts +35 -0
- package/src/plugin/hooks.ts +204 -0
- package/src/plugin/index.ts +63 -0
- package/src/plugin/loader.ts +111 -0
- package/src/plugin/manager.ts +136 -0
- package/src/plugin/registry.ts +145 -0
- package/src/plugin/skills.ts +62 -0
- package/src/plugin/types.ts +172 -0
- package/src/portbroker/bind-with-forward.ts +102 -0
- package/src/portbroker/container-server.ts +305 -0
- package/src/portbroker/forward-result-bus.ts +36 -0
- package/src/portbroker/hostd-client.ts +443 -0
- package/src/portbroker/index.ts +33 -0
- package/src/portbroker/policy.ts +24 -0
- package/src/portbroker/proc-net-tcp.ts +72 -0
- package/src/portbroker/protocol.ts +39 -0
- package/src/reload/client.ts +59 -0
- package/src/reload/index.ts +3 -0
- package/src/reload/registry.ts +60 -0
- package/src/reload/types.ts +13 -0
- package/src/run/bundled-plugins.ts +24 -0
- package/src/run/channel-session-factory.ts +105 -0
- package/src/run/index.ts +432 -0
- package/src/run/plugin-runtime.ts +43 -0
- package/src/run/schema-with-plugins.ts +14 -0
- package/src/secrets/index.ts +13 -0
- package/src/secrets/migrate.ts +95 -0
- package/src/secrets/schema.ts +75 -0
- package/src/secrets/storage.ts +231 -0
- package/src/server/index.ts +436 -0
- package/src/sessions/index.ts +23 -0
- package/src/shared/index.ts +9 -0
- package/src/shared/local-time.ts +21 -0
- package/src/shared/protocol.ts +25 -0
- package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +87 -0
- package/src/skills/typeclaw-channel-telegram-bot/SKILL.md +64 -0
- package/src/skills/typeclaw-config/SKILL.md +643 -0
- package/src/skills/typeclaw-cron/SKILL.md +159 -0
- package/src/skills/typeclaw-git/SKILL.md +89 -0
- package/src/skills/typeclaw-memory/SKILL.md +174 -0
- package/src/skills/typeclaw-monorepo/SKILL.md +175 -0
- package/src/skills/typeclaw-plugins/SKILL.md +594 -0
- package/src/skills/typeclaw-skills/SKILL.md +246 -0
- package/src/stream/broker.ts +161 -0
- package/src/stream/index.ts +16 -0
- package/src/stream/types.ts +69 -0
- package/src/tui/client.ts +45 -0
- package/src/tui/format.ts +317 -0
- package/src/tui/index.ts +225 -0
- package/src/tui/theme.ts +41 -0
- package/typeclaw.schema.json +826 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
const MAX_LISTED_PATHS = 10
|
|
5
|
+
|
|
6
|
+
// `sessions/` is auto-snapshotted and `memory/` is force-committed by the
|
|
7
|
+
// dreaming subagent — both are runtime-owned, never agent-owned. Nudging the
|
|
8
|
+
// agent to commit them would mislead it into staging files outside its remit.
|
|
9
|
+
const RUNTIME_OWNED_PREFIXES = ['sessions/', 'memory/']
|
|
10
|
+
|
|
11
|
+
export type GitNudgeDeps = {
|
|
12
|
+
readStatus: (agentDir: string) => Promise<readonly string[] | null>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Returns "" (not a placeholder string) when there is nothing to nudge about.
|
|
16
|
+
// The empty case must add zero bytes to the system prompt so cache prefixes
|
|
17
|
+
// stay identical to a clean-worktree agent folder.
|
|
18
|
+
export async function renderGitNudge(agentDir: string, deps: GitNudgeDeps = defaultDeps): Promise<string> {
|
|
19
|
+
if (!existsSync(join(agentDir, '.git'))) return ''
|
|
20
|
+
const status = await deps.readStatus(agentDir)
|
|
21
|
+
if (status === null) return ''
|
|
22
|
+
const dirty = filterAgentOwned(status)
|
|
23
|
+
if (dirty.length === 0) return ''
|
|
24
|
+
return formatNudge(dirty)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function formatNudge(dirtyPaths: readonly string[]): string {
|
|
28
|
+
const total = dirtyPaths.length
|
|
29
|
+
const shown = dirtyPaths.slice(0, MAX_LISTED_PATHS)
|
|
30
|
+
const remaining = total - shown.length
|
|
31
|
+
|
|
32
|
+
const lines = [
|
|
33
|
+
'## Uncommitted changes at session start',
|
|
34
|
+
'',
|
|
35
|
+
`git reports ${total} uncommitted file${total === 1 ? '' : 's'} in your agent folder right now:`,
|
|
36
|
+
'',
|
|
37
|
+
...shown.map((p) => `- ${p}`),
|
|
38
|
+
]
|
|
39
|
+
if (remaining > 0) {
|
|
40
|
+
lines.push(`- … and ${remaining} more`)
|
|
41
|
+
}
|
|
42
|
+
lines.push(
|
|
43
|
+
'',
|
|
44
|
+
"These are real, current modifications — not advice. Before declaring this session's task done, commit any of these you're responsible for, with `git add <paths>` and `git commit -m \"…\"` per the version-control rules above. If a listed path is from earlier work you didn't touch, leave it alone.",
|
|
45
|
+
)
|
|
46
|
+
return lines.join('\n')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Porcelain v1 line shape: "XY <path>" or, for renames, "XY <orig> -> <dest>".
|
|
50
|
+
// We drop the status code and, on rename, return the destination because that
|
|
51
|
+
// is the live file the agent would `git add`.
|
|
52
|
+
export function parsePorcelain(stdout: string): string[] {
|
|
53
|
+
const out: string[] = []
|
|
54
|
+
for (const raw of stdout.split('\n')) {
|
|
55
|
+
if (raw.length < 4) continue
|
|
56
|
+
const rest = raw.slice(3)
|
|
57
|
+
const arrowIdx = rest.indexOf(' -> ')
|
|
58
|
+
out.push(arrowIdx === -1 ? rest : rest.slice(arrowIdx + 4))
|
|
59
|
+
}
|
|
60
|
+
return out
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function filterAgentOwned(paths: readonly string[]): string[] {
|
|
64
|
+
return paths.filter((p) => !RUNTIME_OWNED_PREFIXES.some((prefix) => p.startsWith(prefix)))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Mirrors the spawn pattern in `src/container/start.ts` `commitSystemFile`.
|
|
68
|
+
const defaultDeps: GitNudgeDeps = {
|
|
69
|
+
async readStatus(agentDir) {
|
|
70
|
+
const bun = getBun()
|
|
71
|
+
if (!bun) return null
|
|
72
|
+
try {
|
|
73
|
+
const proc = bun.spawn({
|
|
74
|
+
cmd: ['git', 'status', '--porcelain=v1'],
|
|
75
|
+
cwd: agentDir,
|
|
76
|
+
stdout: 'pipe',
|
|
77
|
+
stderr: 'pipe',
|
|
78
|
+
})
|
|
79
|
+
const exit = await proc.exited
|
|
80
|
+
if (exit !== 0) return null
|
|
81
|
+
const text = await new Response(proc.stdout).text()
|
|
82
|
+
return parsePorcelain(text)
|
|
83
|
+
} catch {
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Pieces of `@/agent` are exercised under Node in some tests where
|
|
90
|
+
// `globalThis.Bun` is undefined; this fallback matches the helper in
|
|
91
|
+
// `src/container/start.ts`.
|
|
92
|
+
function getBun(): typeof Bun | null {
|
|
93
|
+
const g = globalThis as { Bun?: typeof Bun }
|
|
94
|
+
return g.Bun ?? null
|
|
95
|
+
}
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import { createAgentSession, DefaultResourceLoader, SessionManager } from '@mariozechner/pi-coding-agent'
|
|
6
|
+
import type { AgentSession, ToolDefinition } from '@mariozechner/pi-coding-agent'
|
|
7
|
+
|
|
8
|
+
import type { ChannelRouter } from '@/channels/router'
|
|
9
|
+
import { getConfig, resolveModel } from '@/config'
|
|
10
|
+
import type {
|
|
11
|
+
BuiltinToolRef,
|
|
12
|
+
HookBus,
|
|
13
|
+
MaterializedSkills,
|
|
14
|
+
PluginRegistry,
|
|
15
|
+
RegisteredTool as PluginRegisteredTool,
|
|
16
|
+
Tool as PluginTool,
|
|
17
|
+
} from '@/plugin'
|
|
18
|
+
import { materializeSkills } from '@/plugin'
|
|
19
|
+
import type { ReloadRegistry } from '@/reload'
|
|
20
|
+
import type { Stream } from '@/stream'
|
|
21
|
+
|
|
22
|
+
import { getAuth } from './auth'
|
|
23
|
+
import { createCompactionSettingsManager } from './compaction'
|
|
24
|
+
import { renderGitNudge } from './git-nudge'
|
|
25
|
+
import { resolveBuiltinToolRefs, wrapPluginTool, wrapSystemAgentTool, wrapSystemTool } from './plugin-tools'
|
|
26
|
+
import { createReloadTool } from './reload-tool'
|
|
27
|
+
import { loadSelf } from './self'
|
|
28
|
+
import { renderSessionOrigin, type SessionOrigin } from './session-origin'
|
|
29
|
+
import { DEFAULT_SYSTEM_PROMPT } from './system-prompt'
|
|
30
|
+
import { createChannelFetchAttachmentTool } from './tools/channel-fetch-attachment'
|
|
31
|
+
import { createChannelHistoryTool } from './tools/channel-history'
|
|
32
|
+
import { createChannelReplyTool } from './tools/channel-reply'
|
|
33
|
+
import { createChannelSendTool } from './tools/channel-send'
|
|
34
|
+
import { createRestartTool } from './tools/restart'
|
|
35
|
+
import { createStreamSnapshotTool } from './tools/stream-snapshot'
|
|
36
|
+
import { webfetchTool } from './tools/webfetch'
|
|
37
|
+
import { websearchTool } from './tools/websearch'
|
|
38
|
+
|
|
39
|
+
export type { SessionOrigin } from './session-origin'
|
|
40
|
+
|
|
41
|
+
export type { AgentSession }
|
|
42
|
+
|
|
43
|
+
type AgentSessionTools = NonNullable<Parameters<typeof createAgentSession>[0]>['tools']
|
|
44
|
+
|
|
45
|
+
export type PluginSessionWiring = {
|
|
46
|
+
registry: PluginRegistry
|
|
47
|
+
hooks: HookBus
|
|
48
|
+
sessionId: string
|
|
49
|
+
agentDir: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type PluginSubagentSelection = {
|
|
53
|
+
pluginName: string
|
|
54
|
+
toolRefs?: BuiltinToolRef[]
|
|
55
|
+
customTools?: PluginTool<any>[]
|
|
56
|
+
toolNamePrefix: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type CreateSessionOptions = {
|
|
60
|
+
reloadRegistry?: ReloadRegistry
|
|
61
|
+
sessionManager?: SessionManager
|
|
62
|
+
stream?: Stream
|
|
63
|
+
channelRouter?: ChannelRouter
|
|
64
|
+
// Bypass the file-based resource loader (IDENTITY.md, SOUL.md, MEMORY.md,
|
|
65
|
+
// memory/, bundled skills) and use this string verbatim as the system prompt.
|
|
66
|
+
systemPromptOverride?: string
|
|
67
|
+
// Identifies the kind of session and (for channels) its addressing fields.
|
|
68
|
+
// Rendered into the system prompt so the agent knows who's listening, where
|
|
69
|
+
// its output goes, and what to pass to channel_send.
|
|
70
|
+
origin?: SessionOrigin
|
|
71
|
+
tools?: AgentSessionTools
|
|
72
|
+
customTools?: ToolDefinition[]
|
|
73
|
+
plugins?: PluginSessionWiring
|
|
74
|
+
// When set, only the named plugin subagent's own tools are exposed; the
|
|
75
|
+
// wider plugin registry's tools are NOT injected. Used by plugin subagent
|
|
76
|
+
// session creation so subagents see exactly what they declared.
|
|
77
|
+
pluginSubagent?: PluginSubagentSelection
|
|
78
|
+
// Enables the `restart` tool. Set when the agent is running inside a
|
|
79
|
+
// typeclaw-managed container. Read from TYPECLAW_CONTAINER_NAME at the call site.
|
|
80
|
+
containerName?: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type CreateSessionResult = {
|
|
84
|
+
session: AgentSession
|
|
85
|
+
dispose: () => Promise<void>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function createSession(options: CreateSessionOptions = {}): Promise<AgentSession> {
|
|
89
|
+
const { session } = await createSessionWithDispose(options)
|
|
90
|
+
return session
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function createSessionWithDispose(options: CreateSessionOptions = {}): Promise<CreateSessionResult> {
|
|
94
|
+
const { authStorage, modelRegistry } = getAuth()
|
|
95
|
+
|
|
96
|
+
const materializedSkills =
|
|
97
|
+
options.plugins && options.plugins.registry.skills.length > 0
|
|
98
|
+
? await materializeSkills(
|
|
99
|
+
options.plugins.registry.skills.map((s) => ({
|
|
100
|
+
pluginName: s.pluginName,
|
|
101
|
+
localName: s.localName,
|
|
102
|
+
skill: s.skill,
|
|
103
|
+
})),
|
|
104
|
+
)
|
|
105
|
+
: null
|
|
106
|
+
|
|
107
|
+
const resourceLoader =
|
|
108
|
+
options.systemPromptOverride !== undefined
|
|
109
|
+
? await createOverrideResourceLoader(options.systemPromptOverride, options.origin)
|
|
110
|
+
: await createResourceLoader({
|
|
111
|
+
...(options.plugins ? { plugins: options.plugins, materializedSkills } : {}),
|
|
112
|
+
...(options.origin ? { origin: options.origin } : {}),
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const subagentBuiltinTools = options.pluginSubagent?.toolRefs
|
|
116
|
+
? resolveBuiltinToolRefs(options.pluginSubagent.toolRefs)
|
|
117
|
+
: undefined
|
|
118
|
+
const pluginCustomTools = options.pluginSubagent
|
|
119
|
+
? wrapSubagentCustomTools(options.pluginSubagent, options.plugins)
|
|
120
|
+
: wrapRegistryTools(options.plugins)
|
|
121
|
+
|
|
122
|
+
const tools = wrapSystemAgentTools(
|
|
123
|
+
options.tools ?? (subagentBuiltinTools as AgentSessionTools | undefined),
|
|
124
|
+
options.plugins,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
// Hoisted above tool construction so the restart tool can be wired with the
|
|
128
|
+
// session's stable identity (sessionManager.getSessionId()). Subscribers use
|
|
129
|
+
// that ID to distinguish the originating session from siblings on the
|
|
130
|
+
// container-restarting broadcast.
|
|
131
|
+
const sessionManager = options.sessionManager ?? SessionManager.inMemory()
|
|
132
|
+
|
|
133
|
+
const customSystemTools =
|
|
134
|
+
options.customTools !== undefined
|
|
135
|
+
? options.customTools
|
|
136
|
+
: options.pluginSubagent
|
|
137
|
+
? []
|
|
138
|
+
: [
|
|
139
|
+
websearchTool,
|
|
140
|
+
webfetchTool,
|
|
141
|
+
...(options.reloadRegistry ? [createReloadTool({ registry: options.reloadRegistry })] : []),
|
|
142
|
+
...(options.stream ? [createStreamSnapshotTool({ stream: options.stream })] : []),
|
|
143
|
+
...buildChannelTools(options.channelRouter, options.origin),
|
|
144
|
+
...(options.containerName
|
|
145
|
+
? [
|
|
146
|
+
createRestartTool({
|
|
147
|
+
containerName: options.containerName,
|
|
148
|
+
originatingSessionId: sessionManager.getSessionId(),
|
|
149
|
+
...(options.stream ? { stream: options.stream } : {}),
|
|
150
|
+
}),
|
|
151
|
+
]
|
|
152
|
+
: []),
|
|
153
|
+
]
|
|
154
|
+
const customTools = [...wrapSystemTools(customSystemTools, options.plugins), ...pluginCustomTools]
|
|
155
|
+
|
|
156
|
+
const model = resolveModel(getConfig().model)
|
|
157
|
+
const { session } = await createAgentSession({
|
|
158
|
+
model,
|
|
159
|
+
sessionManager,
|
|
160
|
+
settingsManager: createCompactionSettingsManager(model),
|
|
161
|
+
authStorage,
|
|
162
|
+
modelRegistry,
|
|
163
|
+
resourceLoader,
|
|
164
|
+
...(tools ? { tools } : {}),
|
|
165
|
+
customTools,
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const unsubRestart = subscribeRestartNotice(options.stream, sessionManager)
|
|
169
|
+
|
|
170
|
+
const dispose = async () => {
|
|
171
|
+
unsubRestart?.()
|
|
172
|
+
if (materializedSkills) await materializedSkills.dispose()
|
|
173
|
+
}
|
|
174
|
+
return { session, dispose }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Subscribes the given session to the in-process broadcast that the `restart`
|
|
178
|
+
// tool fires on a successful hostd ACK. The subscriber dispatches by identity:
|
|
179
|
+
// the session whose tool execution fired the restart (originator) gets a
|
|
180
|
+
// `typeclaw.restart-self` notice instructing the model to proactively confirm
|
|
181
|
+
// restart completion in its very next reply. All other sessions (siblings) get
|
|
182
|
+
// the `typeclaw.restart` notice instructing them not to mention the restart
|
|
183
|
+
// unless directly asked. Two distinct customTypes let downstream consumers
|
|
184
|
+
// distinguish the cases unambiguously. display:false keeps either entry out of
|
|
185
|
+
// any TUI rendering that might inspect the JSONL later. Exported so unit tests
|
|
186
|
+
// can verify the wiring without going through createAgentSession (which needs
|
|
187
|
+
// auth and model registry); the composition test at the bottom of this
|
|
188
|
+
// module's test file covers originator + siblings end to end.
|
|
189
|
+
export function subscribeRestartNotice(
|
|
190
|
+
stream: Stream | undefined,
|
|
191
|
+
sessionManager: SessionManager,
|
|
192
|
+
): (() => void) | null {
|
|
193
|
+
if (!stream) return null
|
|
194
|
+
const unsub = stream.subscribe({ target: { kind: 'broadcast' } }, (msg) => {
|
|
195
|
+
const payload = msg.payload as { kind?: unknown; restartedAt?: unknown; originatingSessionId?: unknown } | null
|
|
196
|
+
if (!payload || payload.kind !== 'container-restarting') return
|
|
197
|
+
if (typeof payload.restartedAt !== 'string') return
|
|
198
|
+
if (typeof payload.originatingSessionId !== 'string') return
|
|
199
|
+
if (payload.originatingSessionId === sessionManager.getSessionId()) {
|
|
200
|
+
sessionManager.appendCustomMessageEntry(
|
|
201
|
+
'typeclaw.restart-self',
|
|
202
|
+
formatRestartNoticeOriginating(payload.restartedAt),
|
|
203
|
+
false,
|
|
204
|
+
)
|
|
205
|
+
} else {
|
|
206
|
+
sessionManager.appendCustomMessageEntry('typeclaw.restart', formatRestartNotice(payload.restartedAt), false)
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
return unsub
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Convention documented in src/channels/router.ts:996-1013: runtime-injected
|
|
213
|
+
// content in the user turn must use the `**[SYSTEM MESSAGE — not from a human]**`
|
|
214
|
+
// framing fenced by `---`, plus an explicit "do not acknowledge or reply"
|
|
215
|
+
// line. Without it, persona-rich models read the heading as a human-authored
|
|
216
|
+
// instruction and reply to it on the next unrelated message.
|
|
217
|
+
export function formatRestartNotice(restartedAt: string): string {
|
|
218
|
+
return [
|
|
219
|
+
'---',
|
|
220
|
+
'**[SYSTEM MESSAGE — not from a human]**',
|
|
221
|
+
'',
|
|
222
|
+
`The TypeClaw container was restarted at ${restartedAt}. The previous session`,
|
|
223
|
+
'state was preserved on disk and you have been resumed inside a new container',
|
|
224
|
+
'process. **Do not acknowledge or reply to this notice unless a human directly',
|
|
225
|
+
'asks whether the restart happened.**',
|
|
226
|
+
'',
|
|
227
|
+
'Guidance:',
|
|
228
|
+
'- If a human asks whether you actually restarted, you may confirm: yes, you',
|
|
229
|
+
` did restart at ${restartedAt}.`,
|
|
230
|
+
'- Otherwise, continue the conversation normally.',
|
|
231
|
+
'',
|
|
232
|
+
'---',
|
|
233
|
+
'',
|
|
234
|
+
].join('\n')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Variant for the session that called the `restart` tool. The user explicitly
|
|
238
|
+
// asked this conversation to restart; staying silent after the reboot is the
|
|
239
|
+
// reported bug ("뭐야 너네 재시작 한 것도 모르냐"). This notice instructs the
|
|
240
|
+
// model to acknowledge restart completion in its very next reply — once — then
|
|
241
|
+
// stop mentioning it. Same SYSTEM MESSAGE framing as the sibling notice so
|
|
242
|
+
// persona-rich models don't reply to the framing itself.
|
|
243
|
+
export function formatRestartNoticeOriginating(restartedAt: string): string {
|
|
244
|
+
return [
|
|
245
|
+
'---',
|
|
246
|
+
'**[SYSTEM MESSAGE — not from a human]**',
|
|
247
|
+
'',
|
|
248
|
+
`The TypeClaw container was restarted at ${restartedAt} at the user's explicit`,
|
|
249
|
+
'request via the `restart` tool. The restart completed successfully and you',
|
|
250
|
+
'have been resumed inside a new container process with your previous',
|
|
251
|
+
'conversation memory intact.',
|
|
252
|
+
'',
|
|
253
|
+
'**Your very next reply must briefly confirm the restart completed** (e.g.',
|
|
254
|
+
'"restart finished, I\'m back" — or in whatever voice fits your persona),',
|
|
255
|
+
"even if the user's next message is about something unrelated. After that",
|
|
256
|
+
"single confirmation, address whatever the user's next message says, and do",
|
|
257
|
+
'not mention the restart again unless the user explicitly asks about it.',
|
|
258
|
+
'',
|
|
259
|
+
'---',
|
|
260
|
+
'',
|
|
261
|
+
].join('\n')
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Builds the channel tool subset: channel_send (always when a router is
|
|
265
|
+
// available), plus channel_reply + channel_history (only when the session
|
|
266
|
+
// origin is a channel — those rely on origin-bound addressing). Extracted
|
|
267
|
+
// from createSessionWithDispose so composition can be unit-tested without
|
|
268
|
+
// going through createAgentSession / auth.
|
|
269
|
+
export function buildChannelTools(
|
|
270
|
+
channelRouter: ChannelRouter | undefined,
|
|
271
|
+
origin: SessionOrigin | undefined,
|
|
272
|
+
): ToolDefinition[] {
|
|
273
|
+
if (!channelRouter) return []
|
|
274
|
+
const tools: ToolDefinition[] = []
|
|
275
|
+
if (origin?.kind === 'channel') {
|
|
276
|
+
const channelOrigin = {
|
|
277
|
+
adapter: origin.adapter,
|
|
278
|
+
workspace: origin.workspace,
|
|
279
|
+
chat: origin.chat,
|
|
280
|
+
thread: origin.thread,
|
|
281
|
+
}
|
|
282
|
+
tools.push(createChannelReplyTool({ router: channelRouter, origin: channelOrigin }))
|
|
283
|
+
tools.push(createChannelHistoryTool({ router: channelRouter, origin: channelOrigin }))
|
|
284
|
+
tools.push(createChannelSendTool({ router: channelRouter, origin: channelOrigin }))
|
|
285
|
+
tools.push(
|
|
286
|
+
createChannelFetchAttachmentTool({
|
|
287
|
+
router: channelRouter,
|
|
288
|
+
origin: { adapter: origin.adapter },
|
|
289
|
+
}),
|
|
290
|
+
)
|
|
291
|
+
} else {
|
|
292
|
+
tools.push(createChannelSendTool({ router: channelRouter }))
|
|
293
|
+
}
|
|
294
|
+
return tools
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function wrapRegistryTools(plugins: PluginSessionWiring | undefined): ToolDefinition[] {
|
|
298
|
+
if (!plugins) return []
|
|
299
|
+
return plugins.registry.tools.map((t: PluginRegisteredTool) =>
|
|
300
|
+
wrapPluginTool(t.tool, {
|
|
301
|
+
pluginName: t.pluginName,
|
|
302
|
+
toolName: t.toolName,
|
|
303
|
+
agentDir: plugins.agentDir,
|
|
304
|
+
sessionId: plugins.sessionId,
|
|
305
|
+
logger: t.logger,
|
|
306
|
+
hooks: plugins.hooks,
|
|
307
|
+
}),
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function wrapSystemAgentTools(
|
|
312
|
+
tools: AgentSessionTools | undefined,
|
|
313
|
+
plugins: PluginSessionWiring | undefined,
|
|
314
|
+
): AgentSessionTools | undefined {
|
|
315
|
+
if (!tools || !hasToolHooks(plugins)) return tools
|
|
316
|
+
return tools.map((tool) =>
|
|
317
|
+
wrapSystemAgentTool(tool, {
|
|
318
|
+
agentDir: plugins.agentDir,
|
|
319
|
+
sessionId: plugins.sessionId,
|
|
320
|
+
hooks: plugins.hooks,
|
|
321
|
+
}),
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function wrapSystemTools(tools: ToolDefinition[], plugins: PluginSessionWiring | undefined): ToolDefinition[] {
|
|
326
|
+
if (!hasToolHooks(plugins)) return tools
|
|
327
|
+
return tools.map((tool) =>
|
|
328
|
+
wrapSystemTool(tool, {
|
|
329
|
+
agentDir: plugins.agentDir,
|
|
330
|
+
sessionId: plugins.sessionId,
|
|
331
|
+
hooks: plugins.hooks,
|
|
332
|
+
}),
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function hasToolHooks(plugins: PluginSessionWiring | undefined): plugins is PluginSessionWiring {
|
|
337
|
+
if (!plugins) return false
|
|
338
|
+
return plugins.hooks.count('tool.before') > 0 || plugins.hooks.count('tool.after') > 0
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function wrapSubagentCustomTools(
|
|
342
|
+
selection: PluginSubagentSelection,
|
|
343
|
+
plugins: PluginSessionWiring | undefined,
|
|
344
|
+
): ToolDefinition[] {
|
|
345
|
+
if (!selection.customTools || !plugins) return []
|
|
346
|
+
const logger = makePluginLogger(selection.pluginName)
|
|
347
|
+
return selection.customTools.map((tool, i) =>
|
|
348
|
+
wrapPluginTool(tool, {
|
|
349
|
+
pluginName: selection.pluginName,
|
|
350
|
+
toolName: `${selection.toolNamePrefix}_${i}`,
|
|
351
|
+
agentDir: plugins.agentDir,
|
|
352
|
+
sessionId: plugins.sessionId,
|
|
353
|
+
logger,
|
|
354
|
+
hooks: plugins.hooks,
|
|
355
|
+
}),
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function makePluginLogger(pluginName: string) {
|
|
360
|
+
const prefix = `[plugin:${pluginName}]`
|
|
361
|
+
return {
|
|
362
|
+
info: (m: string) => console.log(`${prefix} ${m}`),
|
|
363
|
+
warn: (m: string) => console.warn(`${prefix} ${m}`),
|
|
364
|
+
error: (m: string) => console.error(`${prefix} ${m}`),
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export async function createOverrideResourceLoader(
|
|
369
|
+
systemPrompt: string,
|
|
370
|
+
origin?: SessionOrigin,
|
|
371
|
+
): Promise<DefaultResourceLoader> {
|
|
372
|
+
const loader = new DefaultResourceLoader({
|
|
373
|
+
systemPromptOverride: () => withOrigin(systemPrompt, origin),
|
|
374
|
+
appendSystemPromptOverride: () => [],
|
|
375
|
+
})
|
|
376
|
+
await loader.reload()
|
|
377
|
+
return loader
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export type CreateResourceLoaderOptions = {
|
|
381
|
+
agentDir?: string
|
|
382
|
+
plugins?: PluginSessionWiring
|
|
383
|
+
materializedSkills?: MaterializedSkills | null
|
|
384
|
+
origin?: SessionOrigin
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export async function createResourceLoader(options: CreateResourceLoaderOptions = {}): Promise<DefaultResourceLoader> {
|
|
388
|
+
const agentDir = options.agentDir ?? process.cwd()
|
|
389
|
+
const self = await loadSelf(agentDir)
|
|
390
|
+
let systemPrompt = `${DEFAULT_SYSTEM_PROMPT}\n\n${self}`
|
|
391
|
+
|
|
392
|
+
if (options.plugins) {
|
|
393
|
+
const event = { prompt: systemPrompt, sessionId: options.plugins.sessionId, agentDir, origin: options.origin }
|
|
394
|
+
await options.plugins.hooks.runSessionPrompt(event)
|
|
395
|
+
systemPrompt = event.prompt
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Appended last so the dirty-files snapshot is the most-recent context the
|
|
399
|
+
// agent reads, and so its bytes sit in the cache-suffix region rather than
|
|
400
|
+
// splitting the cacheable prefix shared by clean-worktree sessions.
|
|
401
|
+
const gitNudge = await renderGitNudge(agentDir)
|
|
402
|
+
if (gitNudge !== '') {
|
|
403
|
+
systemPrompt = `${systemPrompt}\n\n${gitNudge}`
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const additionalSkillPaths = [getBundledSkillsDir()]
|
|
407
|
+
// pi-coding-agent's DefaultResourceLoader auto-discovers <agentDir>/skills/
|
|
408
|
+
// but not <agentDir>/.agents/skills/. We do not scaffold <agentDir>/skills/
|
|
409
|
+
// and the system prompt no longer advertises it — the only skill directories
|
|
410
|
+
// a TypeClaw agent owns are .agents/skills/ (user-installed) and
|
|
411
|
+
// memory/skills/ (dreaming-owned). Both are wired in explicitly below;
|
|
412
|
+
// anything the upstream loader auto-discovers under <agentDir>/skills/ is
|
|
413
|
+
// outside our supported surface.
|
|
414
|
+
const userInstalledSkillsDir = join(agentDir, '.agents', 'skills')
|
|
415
|
+
if (existsSync(userInstalledSkillsDir)) {
|
|
416
|
+
additionalSkillPaths.push(userInstalledSkillsDir)
|
|
417
|
+
}
|
|
418
|
+
// Muscle-memory skills written by the dreaming subagent. Same auto-discover
|
|
419
|
+
// story as `.agents/skills/` — the loader doesn't walk arbitrary subtrees of
|
|
420
|
+
// the agent dir, so we wire this in explicitly. Existence-gated so a session
|
|
421
|
+
// that has never dreamed doesn't pay for an empty path.
|
|
422
|
+
const muscleMemorySkillsDir = join(agentDir, 'memory', 'skills')
|
|
423
|
+
if (existsSync(muscleMemorySkillsDir)) {
|
|
424
|
+
additionalSkillPaths.push(muscleMemorySkillsDir)
|
|
425
|
+
}
|
|
426
|
+
if (options.plugins) {
|
|
427
|
+
for (const dir of options.plugins.registry.skillsDirs) {
|
|
428
|
+
additionalSkillPaths.push(dir.path)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (options.materializedSkills) {
|
|
432
|
+
additionalSkillPaths.push(options.materializedSkills.dir)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const loader = new DefaultResourceLoader({
|
|
436
|
+
systemPromptOverride: () => withOrigin(systemPrompt, options.origin),
|
|
437
|
+
appendSystemPromptOverride: () => [],
|
|
438
|
+
additionalSkillPaths,
|
|
439
|
+
})
|
|
440
|
+
await loader.reload()
|
|
441
|
+
return loader
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function withOrigin(systemPrompt: string, origin: SessionOrigin | undefined): string {
|
|
445
|
+
if (!origin) return systemPrompt
|
|
446
|
+
return `${systemPrompt}\n\n${renderSessionOrigin(origin)}`
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function getBundledSkillsDir(): string {
|
|
450
|
+
return join(dirname(fileURLToPath(import.meta.url)), '..', 'skills')
|
|
451
|
+
}
|