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,60 @@
|
|
|
1
|
+
import type { Reloadable, ReloadAllResult, ReloadResult } from './types'
|
|
2
|
+
|
|
3
|
+
export class ReloadRegistry {
|
|
4
|
+
private items = new Map<string, Reloadable>()
|
|
5
|
+
|
|
6
|
+
register(item: Reloadable): void {
|
|
7
|
+
if (this.items.has(item.scope)) {
|
|
8
|
+
throw new Error(`reload scope "${item.scope}" is already registered`)
|
|
9
|
+
}
|
|
10
|
+
this.items.set(item.scope, item)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
has(scope: string): boolean {
|
|
14
|
+
return this.items.has(scope)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get(scope: string): Reloadable | undefined {
|
|
18
|
+
return this.items.get(scope)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
list(): Reloadable[] {
|
|
22
|
+
return Array.from(this.items.values())
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Runs serially in registration order. Reloadables observe the side
|
|
26
|
+
// effects of earlier ones — e.g. cron reload reads the freshly swapped
|
|
27
|
+
// config when it runs after the config reloadable. Manual reload is rare,
|
|
28
|
+
// so deterministic ordering wins over parallelism.
|
|
29
|
+
async reloadAll(): Promise<ReloadAllResult> {
|
|
30
|
+
const results: ReloadResult[] = []
|
|
31
|
+
for (const item of this.list()) {
|
|
32
|
+
try {
|
|
33
|
+
results.push(await item.reload())
|
|
34
|
+
} catch (err) {
|
|
35
|
+
results.push({ scope: item.scope, ok: false, reason: errorMessage(err) })
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { results }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async reloadOne(scope: string): Promise<ReloadResult> {
|
|
42
|
+
const item = this.items.get(scope)
|
|
43
|
+
if (!item) return { scope, ok: false, reason: `unknown scope: ${scope}` }
|
|
44
|
+
try {
|
|
45
|
+
return await item.reload()
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return { scope, ok: false, reason: errorMessage(err) }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function errorMessage(err: unknown): string {
|
|
53
|
+
if (err instanceof Error) return err.message
|
|
54
|
+
if (typeof err === 'string') return err
|
|
55
|
+
try {
|
|
56
|
+
return JSON.stringify(err)
|
|
57
|
+
} catch {
|
|
58
|
+
return String(err)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type ReloadResult =
|
|
2
|
+
| { scope: string; ok: true; summary: string; details?: unknown }
|
|
3
|
+
| { scope: string; ok: false; reason: string }
|
|
4
|
+
|
|
5
|
+
export type Reloadable = {
|
|
6
|
+
scope: string
|
|
7
|
+
description: string
|
|
8
|
+
reload: () => Promise<ReloadResult>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type ReloadAllResult = {
|
|
12
|
+
results: ReloadResult[]
|
|
13
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import agentBrowserPlugin from '@/bundled-plugins/agent-browser'
|
|
2
|
+
import guardPlugin from '@/bundled-plugins/guard'
|
|
3
|
+
import memoryPlugin from '@/bundled-plugins/memory'
|
|
4
|
+
import securityPlugin from '@/bundled-plugins/security'
|
|
5
|
+
import type { ResolvedPlugin } from '@/plugin'
|
|
6
|
+
|
|
7
|
+
// Consumed by both `startAgent` (auto-loaded before user plugins) AND
|
|
8
|
+
// `scripts/generate-schema.ts` (each entry's `defined.configSchema` is merged
|
|
9
|
+
// into `typeclaw.schema.json` keyed by plugin name). Adding a bundled plugin
|
|
10
|
+
// here automatically extends the JSON schema; core `configSchema` does not
|
|
11
|
+
// need to know about plugin-owned blocks.
|
|
12
|
+
//
|
|
13
|
+
// Order matters: `security` is listed first because its `tool.before` hook
|
|
14
|
+
// must get first refusal on every tool call (HookBus runs hooks in
|
|
15
|
+
// registration order and short-circuits on the first `{ block: true }`).
|
|
16
|
+
// Letting `guard` run first would still work today since the two plugins
|
|
17
|
+
// guard disjoint surfaces, but seeding the order now means future overlap
|
|
18
|
+
// (e.g. a security policy on writes) blocks before guard's softer advice.
|
|
19
|
+
export const BUNDLED_PLUGINS: ResolvedPlugin[] = [
|
|
20
|
+
{ name: 'security', version: undefined, source: '<bundled>', defined: securityPlugin },
|
|
21
|
+
{ name: 'guard', version: undefined, source: '<bundled>', defined: guardPlugin },
|
|
22
|
+
{ name: 'memory', version: undefined, source: '<bundled>', defined: memoryPlugin },
|
|
23
|
+
{ name: 'agent-browser', version: undefined, source: '<bundled>', defined: agentBrowserPlugin },
|
|
24
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { SessionManager } from '@mariozechner/pi-coding-agent'
|
|
2
|
+
|
|
3
|
+
import { createSession as defaultCreateSession } from '@/agent'
|
|
4
|
+
import type { CreateSessionForChannel, ChannelRouter } from '@/channels'
|
|
5
|
+
import type { ReloadRegistry } from '@/reload'
|
|
6
|
+
import type { SessionFactory } from '@/sessions'
|
|
7
|
+
import type { Stream } from '@/stream'
|
|
8
|
+
|
|
9
|
+
import type { PluginRuntime } from './plugin-runtime'
|
|
10
|
+
|
|
11
|
+
export type BuildChannelSessionFactoryDeps = {
|
|
12
|
+
cwd: string
|
|
13
|
+
sessionFactory: SessionFactory
|
|
14
|
+
stream: Stream
|
|
15
|
+
reloadRegistry: ReloadRegistry
|
|
16
|
+
pluginRuntime: PluginRuntime
|
|
17
|
+
// Late-bound: the router is constructed by the channel manager which itself
|
|
18
|
+
// takes this factory. Reading the router lazily breaks the construction
|
|
19
|
+
// cycle while still ensuring the factory's sessions get the same router
|
|
20
|
+
// their inbound messages came from.
|
|
21
|
+
getChannelRouter: () => ChannelRouter
|
|
22
|
+
containerName?: string
|
|
23
|
+
// Test seam: lets a fake stand in for the agent session creator so tests
|
|
24
|
+
// can assert exactly which CreateSessionOptions the factory builds without
|
|
25
|
+
// needing a live LLM, plugin runtime, or session manager on disk.
|
|
26
|
+
createSession?: typeof defaultCreateSession
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The production wiring for channel-routed sessions. Channel inbounds arrive
|
|
30
|
+
// at the router, the router calls this factory to get an AgentSession, and
|
|
31
|
+
// the agent uses `channel_send` to reply. If `channelRouter` is missing here
|
|
32
|
+
// the agent has no `channel_send` tool and cannot reply — silently. That was
|
|
33
|
+
// the bug this factory exists to prevent. The shape of these options must
|
|
34
|
+
// stay aligned with createSessionForCron in src/run/index.ts; both are
|
|
35
|
+
// "channel-aware" sessions that need the same full plumbing.
|
|
36
|
+
export function buildChannelSessionFactory(deps: BuildChannelSessionFactoryDeps): CreateSessionForChannel {
|
|
37
|
+
const createSession = deps.createSession ?? defaultCreateSession
|
|
38
|
+
return async ({ existingSessionId, existingSessionFile, origin }) => {
|
|
39
|
+
const sessionDir = deps.sessionFactory.sessionDir()
|
|
40
|
+
const sessionManager =
|
|
41
|
+
existingSessionId !== undefined
|
|
42
|
+
? tryReopenOrCreate(deps.cwd, sessionDir, existingSessionId, existingSessionFile)
|
|
43
|
+
: SessionManager.create(deps.cwd, sessionDir)
|
|
44
|
+
|
|
45
|
+
const snap = deps.pluginRuntime.get()
|
|
46
|
+
const session = await createSession({
|
|
47
|
+
reloadRegistry: deps.reloadRegistry,
|
|
48
|
+
sessionManager,
|
|
49
|
+
stream: deps.stream,
|
|
50
|
+
channelRouter: deps.getChannelRouter(),
|
|
51
|
+
origin,
|
|
52
|
+
...(snap.hasAnyPluginContent
|
|
53
|
+
? {
|
|
54
|
+
plugins: {
|
|
55
|
+
registry: snap.registry,
|
|
56
|
+
hooks: snap.hooks,
|
|
57
|
+
sessionId: sessionManager.getSessionId(),
|
|
58
|
+
agentDir: deps.cwd,
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
: {}),
|
|
62
|
+
...(deps.containerName !== undefined ? { containerName: deps.containerName } : {}),
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
session,
|
|
67
|
+
sessionId: sessionManager.getSessionId(),
|
|
68
|
+
dispose: async () => {
|
|
69
|
+
session.dispose()
|
|
70
|
+
},
|
|
71
|
+
...(snap.hasAnyPluginContent ? { hooks: snap.hooks } : {}),
|
|
72
|
+
getTranscriptPath: () => sessionManager.getSessionFile(),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Reopen the persisted session manager when possible so the agent picks up
|
|
78
|
+
// where it left off. We use the persisted basename (sessionFile) directly
|
|
79
|
+
// because pi-coding-agent prefixes filenames with an ISO timestamp at write
|
|
80
|
+
// time that is not derivable from sessionId alone. Failure to reopen
|
|
81
|
+
// (corruption, missing file, schema drift, or v2 mapping with no sessionFile)
|
|
82
|
+
// falls back to a fresh session — matching the router's existing best-effort
|
|
83
|
+
// durability for channel sessions.
|
|
84
|
+
function tryReopenOrCreate(
|
|
85
|
+
cwd: string,
|
|
86
|
+
sessionDir: string,
|
|
87
|
+
existingSessionId: string,
|
|
88
|
+
existingSessionFile: string | undefined,
|
|
89
|
+
): SessionManager {
|
|
90
|
+
if (existingSessionFile === undefined) {
|
|
91
|
+
console.warn(
|
|
92
|
+
`[channels] session ${existingSessionId} has no sessionFile (v2 mapping not yet migrated); creating new`,
|
|
93
|
+
)
|
|
94
|
+
return SessionManager.create(cwd, sessionDir)
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
return SessionManager.open(`${sessionDir}/${existingSessionFile}`)
|
|
98
|
+
} catch (err) {
|
|
99
|
+
const reason = err instanceof Error ? err.message : String(err)
|
|
100
|
+
console.warn(
|
|
101
|
+
`[channels] could not rehydrate session ${existingSessionId} from ${existingSessionFile}: ${reason}; creating new`,
|
|
102
|
+
)
|
|
103
|
+
return SessionManager.create(cwd, sessionDir)
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/run/index.ts
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { SessionManager } from '@mariozechner/pi-coding-agent'
|
|
2
|
+
|
|
3
|
+
import { createSession, createSessionWithDispose } from '@/agent'
|
|
4
|
+
import {
|
|
5
|
+
createSubagentConsumer,
|
|
6
|
+
defaultCreateSessionForSubagent,
|
|
7
|
+
invokeSubagent,
|
|
8
|
+
type Subagent as InternalSubagent,
|
|
9
|
+
type SubagentConsumer,
|
|
10
|
+
type SubagentRegistry,
|
|
11
|
+
} from '@/agent/subagents'
|
|
12
|
+
import { createChannelManager, createChannelsReloadable, type ChannelManager } from '@/channels'
|
|
13
|
+
import { createConfigReloadable, getConfig, loadConfigSync, loadPluginConfigsSync } from '@/config'
|
|
14
|
+
import {
|
|
15
|
+
type CronConsumer,
|
|
16
|
+
type CronJob,
|
|
17
|
+
type CronFile,
|
|
18
|
+
createCronConsumer,
|
|
19
|
+
createCronReloadable,
|
|
20
|
+
createScheduler,
|
|
21
|
+
type LoadCronResult,
|
|
22
|
+
loadCron as loadCronDefault,
|
|
23
|
+
type Scheduler,
|
|
24
|
+
} from '@/cron'
|
|
25
|
+
import { loadPlugins, type LoadPluginsResult, pluginCronJobs, type PluginRegistry, summarizeLoaded } from '@/plugin'
|
|
26
|
+
import { createContainerBroker, publishForwardResult } from '@/portbroker'
|
|
27
|
+
import { ReloadRegistry } from '@/reload'
|
|
28
|
+
import { createServer, type Server } from '@/server'
|
|
29
|
+
import { createSessionFactory, type SessionFactory } from '@/sessions'
|
|
30
|
+
import { createStream, type Stream } from '@/stream'
|
|
31
|
+
import { createTui as createTuiDefault, type TuiOptions } from '@/tui'
|
|
32
|
+
|
|
33
|
+
import { BUNDLED_PLUGINS } from './bundled-plugins'
|
|
34
|
+
import { buildChannelSessionFactory } from './channel-session-factory'
|
|
35
|
+
import { createPluginRuntime, type PluginRuntime, type PluginSubagentEntry } from './plugin-runtime'
|
|
36
|
+
|
|
37
|
+
type BunServer = ReturnType<Server['start']>
|
|
38
|
+
|
|
39
|
+
export type TuiFactory = (options: TuiOptions) => { run: () => Promise<void> }
|
|
40
|
+
|
|
41
|
+
export type LoadCronFn = (agentDir: string, options?: { subagents?: SubagentRegistry }) => Promise<LoadCronResult>
|
|
42
|
+
export type SchedulerFactory = (options: { cwd: string; file: CronFile; onFire: (job: CronJob) => void }) => Scheduler
|
|
43
|
+
|
|
44
|
+
export type StartAgentOptions = {
|
|
45
|
+
port: number
|
|
46
|
+
attachTui: boolean
|
|
47
|
+
initialPrompt?: string
|
|
48
|
+
cwd?: string
|
|
49
|
+
createTui?: TuiFactory
|
|
50
|
+
loadCron?: LoadCronFn
|
|
51
|
+
createSchedulerFor?: SchedulerFactory
|
|
52
|
+
sessionFactory?: SessionFactory
|
|
53
|
+
stream?: Stream
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type StartAgentResult = {
|
|
57
|
+
server: BunServer
|
|
58
|
+
tuiPromise: Promise<void> | null
|
|
59
|
+
scheduler: Scheduler | null
|
|
60
|
+
cronConsumer: CronConsumer | null
|
|
61
|
+
subagentConsumer: SubagentConsumer
|
|
62
|
+
reloadRegistry: ReloadRegistry
|
|
63
|
+
stream: Stream
|
|
64
|
+
pluginRuntime: PluginRuntime
|
|
65
|
+
loadedPlugins: LoadPluginsResult['loadedPlugins']
|
|
66
|
+
channelManager: ChannelManager
|
|
67
|
+
stop: () => void | Promise<void>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function startAgent({
|
|
71
|
+
port,
|
|
72
|
+
attachTui,
|
|
73
|
+
initialPrompt,
|
|
74
|
+
cwd = process.cwd(),
|
|
75
|
+
createTui = createTuiDefault,
|
|
76
|
+
loadCron = loadCronDefault,
|
|
77
|
+
createSchedulerFor,
|
|
78
|
+
sessionFactory = createSessionFactory({ agentDir: cwd }),
|
|
79
|
+
stream = createStream(),
|
|
80
|
+
}: StartAgentOptions): Promise<StartAgentResult> {
|
|
81
|
+
const reloadRegistry = new ReloadRegistry()
|
|
82
|
+
|
|
83
|
+
// The host CLI sets TYPECLAW_CONTAINER_NAME when it `docker run`s us. When
|
|
84
|
+
// running outside a typeclaw container (tests, ad-hoc `bun run typeclaw run`
|
|
85
|
+
// outside docker), the env var is absent and the `restart` tool is omitted —
|
|
86
|
+
// which is what we want, since there is no host daemon to honor it anyway.
|
|
87
|
+
const containerName = process.env.TYPECLAW_CONTAINER_NAME
|
|
88
|
+
const containerNameOpt = containerName !== undefined ? { containerName } : {}
|
|
89
|
+
reloadRegistry.register(createConfigReloadable({ cwd }))
|
|
90
|
+
|
|
91
|
+
const pluginConfigsByName = loadPluginConfigsSync(cwd)
|
|
92
|
+
const cwdConfig = loadConfigSync(cwd)
|
|
93
|
+
const pluginsLoaded = await loadPlugins({
|
|
94
|
+
entries: cwdConfig.plugins,
|
|
95
|
+
agentDir: cwd,
|
|
96
|
+
configsByName: pluginConfigsByName,
|
|
97
|
+
bundled: BUNDLED_PLUGINS,
|
|
98
|
+
})
|
|
99
|
+
const pluginRegistry = pluginsLoaded.registry
|
|
100
|
+
const pluginHooks = pluginsLoaded.hooks
|
|
101
|
+
|
|
102
|
+
const { registry: subagents, pluginSubagentByShim, pluginSubagentByName } = mergeSubagents(pluginRegistry)
|
|
103
|
+
|
|
104
|
+
const hasAnyPluginContent =
|
|
105
|
+
pluginRegistry.tools.length > 0 ||
|
|
106
|
+
pluginRegistry.subagents.length > 0 ||
|
|
107
|
+
pluginRegistry.cronJobs.length > 0 ||
|
|
108
|
+
pluginRegistry.skills.length > 0 ||
|
|
109
|
+
pluginRegistry.skillsDirs.length > 0 ||
|
|
110
|
+
pluginsLoaded.loadedPlugins.length > 0
|
|
111
|
+
|
|
112
|
+
const pluginRuntime = createPluginRuntime({
|
|
113
|
+
registry: pluginRegistry,
|
|
114
|
+
hooks: pluginHooks,
|
|
115
|
+
subagents,
|
|
116
|
+
pluginSubagentByShim,
|
|
117
|
+
hasAnyPluginContent,
|
|
118
|
+
loadedPlugins: pluginsLoaded.loadedPlugins,
|
|
119
|
+
materializedSkills: null,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const channelManager = createChannelManager({
|
|
123
|
+
agentDir: cwd,
|
|
124
|
+
channelsConfigRef: () => getConfig().channels,
|
|
125
|
+
aliasesRef: () => getConfig().alias,
|
|
126
|
+
createSessionForChannel: buildChannelSessionFactory({
|
|
127
|
+
cwd,
|
|
128
|
+
sessionFactory,
|
|
129
|
+
stream,
|
|
130
|
+
reloadRegistry,
|
|
131
|
+
pluginRuntime,
|
|
132
|
+
getChannelRouter: () => channelManager.router,
|
|
133
|
+
...containerNameOpt,
|
|
134
|
+
}),
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const createSessionForSubagent: import('@/agent/subagents').CreateSessionForSubagent = async (
|
|
138
|
+
subagent,
|
|
139
|
+
subagentOptions,
|
|
140
|
+
) => {
|
|
141
|
+
const snap = pluginRuntime.get()
|
|
142
|
+
const entry = snap.pluginSubagentByShim.get(subagent)
|
|
143
|
+
if (entry) {
|
|
144
|
+
const sessionId = `subagent-${entry.pluginName}-${crypto.randomUUID()}`
|
|
145
|
+
const created = await createSessionWithDispose({
|
|
146
|
+
systemPromptOverride: entry.pluginSubagent.systemPrompt,
|
|
147
|
+
channelRouter: channelManager.router,
|
|
148
|
+
origin: {
|
|
149
|
+
kind: 'subagent',
|
|
150
|
+
subagent: subagentOptions?.name ?? entry.subagentName,
|
|
151
|
+
parentSessionId: subagentOptions?.parentSessionId ?? '<unknown>',
|
|
152
|
+
},
|
|
153
|
+
plugins: {
|
|
154
|
+
registry: snap.registry,
|
|
155
|
+
hooks: snap.hooks,
|
|
156
|
+
sessionId,
|
|
157
|
+
agentDir: cwd,
|
|
158
|
+
},
|
|
159
|
+
pluginSubagent: {
|
|
160
|
+
pluginName: entry.pluginName,
|
|
161
|
+
...(entry.pluginSubagent.tools ? { toolRefs: entry.pluginSubagent.tools } : {}),
|
|
162
|
+
...(entry.pluginSubagent.customTools ? { customTools: entry.pluginSubagent.customTools } : {}),
|
|
163
|
+
toolNamePrefix: `__plugin_${entry.pluginName}_${entry.subagentName}`,
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
return {
|
|
167
|
+
...created,
|
|
168
|
+
hooks: snap.hooks,
|
|
169
|
+
sessionId,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return defaultCreateSessionForSubagent(subagent, subagentOptions)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const subagentConsumer = createSubagentConsumer({
|
|
176
|
+
stream,
|
|
177
|
+
getRegistry: () => pluginRuntime.get().subagents,
|
|
178
|
+
agentDir: cwd,
|
|
179
|
+
createSessionForSubagent,
|
|
180
|
+
inFlightKey: (name, payload) => {
|
|
181
|
+
const entry = pluginSubagentByName.get(name)
|
|
182
|
+
const fn = entry?.pluginSubagent.inFlightKey
|
|
183
|
+
if (fn !== undefined) {
|
|
184
|
+
try {
|
|
185
|
+
return `${name}:${fn(payload)}`
|
|
186
|
+
} catch {
|
|
187
|
+
return name
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return name
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
subagentConsumer.start()
|
|
194
|
+
|
|
195
|
+
const cronConsumer = createCronConsumer({
|
|
196
|
+
stream,
|
|
197
|
+
cwd,
|
|
198
|
+
createSessionForCron: async (job) => {
|
|
199
|
+
const snap = pluginRuntime.get()
|
|
200
|
+
const sessionManager = SessionManager.create(cwd, sessionFactory.sessionDir())
|
|
201
|
+
const sessionId = sessionManager.getSessionId()
|
|
202
|
+
const session = await createSession({
|
|
203
|
+
reloadRegistry,
|
|
204
|
+
sessionManager,
|
|
205
|
+
stream,
|
|
206
|
+
channelRouter: channelManager.router,
|
|
207
|
+
origin: { kind: 'cron', jobId: job.id, jobKind: 'prompt' },
|
|
208
|
+
...(snap.hasAnyPluginContent
|
|
209
|
+
? {
|
|
210
|
+
plugins: {
|
|
211
|
+
registry: snap.registry,
|
|
212
|
+
hooks: snap.hooks,
|
|
213
|
+
sessionId,
|
|
214
|
+
agentDir: cwd,
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
: {}),
|
|
218
|
+
...containerNameOpt,
|
|
219
|
+
})
|
|
220
|
+
return {
|
|
221
|
+
prompt: (text) => session.prompt(text),
|
|
222
|
+
dispose: () => session.dispose(),
|
|
223
|
+
sessionId,
|
|
224
|
+
...(snap.hasAnyPluginContent ? { hooks: snap.hooks } : {}),
|
|
225
|
+
getTranscriptPath: () => sessionManager.getSessionFile(),
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
const internalJobs = () => pluginCronJobs(pluginRuntime.get().registry)
|
|
231
|
+
const factory = createSchedulerFor ?? makeDefaultSchedulerFactory(internalJobs)
|
|
232
|
+
const scheduler = await startScheduler({
|
|
233
|
+
cwd,
|
|
234
|
+
loadCron,
|
|
235
|
+
createSchedulerFor: factory,
|
|
236
|
+
stream,
|
|
237
|
+
hasInternalJobs: internalJobs().length > 0,
|
|
238
|
+
getSubagents: () => pluginRuntime.get().subagents,
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
if (scheduler) {
|
|
242
|
+
cronConsumer.start()
|
|
243
|
+
reloadRegistry.register(
|
|
244
|
+
createCronReloadable({ cwd, scheduler, internalJobs, getSubagents: () => pluginRuntime.get().subagents }),
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
reloadRegistry.register(createChannelsReloadable({ manager: channelManager }))
|
|
249
|
+
await channelManager.start()
|
|
250
|
+
|
|
251
|
+
pluginsLoaded.setSpawnSubagent(async (name, payload) => {
|
|
252
|
+
await invokeSubagent(name, {
|
|
253
|
+
registry: pluginRuntime.get().subagents,
|
|
254
|
+
createSessionForSubagent,
|
|
255
|
+
agentDir: cwd,
|
|
256
|
+
userPrompt: '',
|
|
257
|
+
payload,
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
pluginsLoaded.markBooted()
|
|
261
|
+
|
|
262
|
+
if (pluginsLoaded.loadedPlugins.length > 0) {
|
|
263
|
+
console.log(`[plugin] loaded ${summarizeLoaded(pluginsLoaded.loadedPlugins, pluginRegistry)}`)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Container-side portbroker is instantiated only when the host plumbed a
|
|
267
|
+
// broker token in via env var. Outside the container (tests, ad-hoc dev
|
|
268
|
+
// runs), the env var is absent and the broker stays off — same fence as
|
|
269
|
+
// TYPECLAW_CONTAINER_NAME guards the restart tool.
|
|
270
|
+
const brokerTokenEnv = process.env.TYPECLAW_HOSTD_BROKER_TOKEN
|
|
271
|
+
const containerBroker =
|
|
272
|
+
brokerTokenEnv !== undefined && brokerTokenEnv.length > 0
|
|
273
|
+
? createContainerBroker({
|
|
274
|
+
expectedToken: brokerTokenEnv,
|
|
275
|
+
onLog: (event) => {
|
|
276
|
+
if (event.kind === 'subscribed') return
|
|
277
|
+
stream.publish({
|
|
278
|
+
target: { kind: 'broadcast' },
|
|
279
|
+
payload: { kind: 'portbroker-log', event },
|
|
280
|
+
})
|
|
281
|
+
},
|
|
282
|
+
// Re-publish to the in-process bus so consumers (today: the
|
|
283
|
+
// agent-browser plugin's bind-with-forward retry loop) can subscribe
|
|
284
|
+
// without holding a reference to the broker. See src/portbroker/
|
|
285
|
+
// forward-result-bus.ts for the contract.
|
|
286
|
+
onForwardResult: (event) => publishForwardResult(event),
|
|
287
|
+
})
|
|
288
|
+
: undefined
|
|
289
|
+
const containerBrokerOpt = containerBroker ? { containerBroker } : {}
|
|
290
|
+
|
|
291
|
+
const server = createServer({
|
|
292
|
+
port,
|
|
293
|
+
reloadAll: () => reloadRegistry.reloadAll(),
|
|
294
|
+
reloadRegistry,
|
|
295
|
+
sessionFactory,
|
|
296
|
+
stream,
|
|
297
|
+
channelRouter: channelManager.router,
|
|
298
|
+
agentDir: cwd,
|
|
299
|
+
pluginRuntime,
|
|
300
|
+
...containerNameOpt,
|
|
301
|
+
...containerBrokerOpt,
|
|
302
|
+
}).start()
|
|
303
|
+
|
|
304
|
+
let stopped = false
|
|
305
|
+
const stop = async () => {
|
|
306
|
+
if (stopped) return
|
|
307
|
+
stopped = true
|
|
308
|
+
scheduler?.stop()
|
|
309
|
+
cronConsumer.stop()
|
|
310
|
+
subagentConsumer.stop()
|
|
311
|
+
server.stop(true)
|
|
312
|
+
void disposeMaterializedSkills(pluginRuntime)
|
|
313
|
+
await channelManager.stop()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!attachTui) {
|
|
317
|
+
return {
|
|
318
|
+
server,
|
|
319
|
+
tuiPromise: null,
|
|
320
|
+
scheduler,
|
|
321
|
+
cronConsumer: scheduler ? cronConsumer : null,
|
|
322
|
+
subagentConsumer,
|
|
323
|
+
reloadRegistry,
|
|
324
|
+
stream,
|
|
325
|
+
pluginRuntime,
|
|
326
|
+
loadedPlugins: pluginsLoaded.loadedPlugins,
|
|
327
|
+
channelManager,
|
|
328
|
+
stop,
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const url = `ws://localhost:${server.port}`
|
|
333
|
+
const tui = createTui({ url, initialPrompt })
|
|
334
|
+
const tuiPromise = tui.run()
|
|
335
|
+
return {
|
|
336
|
+
server,
|
|
337
|
+
tuiPromise,
|
|
338
|
+
scheduler,
|
|
339
|
+
cronConsumer: scheduler ? cronConsumer : null,
|
|
340
|
+
subagentConsumer,
|
|
341
|
+
reloadRegistry,
|
|
342
|
+
stream,
|
|
343
|
+
pluginRuntime,
|
|
344
|
+
loadedPlugins: pluginsLoaded.loadedPlugins,
|
|
345
|
+
channelManager,
|
|
346
|
+
stop,
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function disposeMaterializedSkills(pluginRuntime: PluginRuntime): Promise<void> {
|
|
351
|
+
const pending = pluginRuntime.drainPendingDisposal()
|
|
352
|
+
const current = pluginRuntime.get().materializedSkills
|
|
353
|
+
const all = current ? [...pending, current] : pending
|
|
354
|
+
await Promise.allSettled(all.map((m) => m.dispose()))
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function startScheduler({
|
|
358
|
+
cwd,
|
|
359
|
+
loadCron,
|
|
360
|
+
createSchedulerFor,
|
|
361
|
+
stream,
|
|
362
|
+
hasInternalJobs,
|
|
363
|
+
getSubagents,
|
|
364
|
+
}: {
|
|
365
|
+
cwd: string
|
|
366
|
+
loadCron: LoadCronFn
|
|
367
|
+
createSchedulerFor: SchedulerFactory
|
|
368
|
+
stream: Stream
|
|
369
|
+
hasInternalJobs: boolean
|
|
370
|
+
getSubagents?: () => SubagentRegistry
|
|
371
|
+
}): Promise<Scheduler | null> {
|
|
372
|
+
let result: LoadCronResult
|
|
373
|
+
const subagents = getSubagents?.()
|
|
374
|
+
try {
|
|
375
|
+
result = await loadCron(cwd, subagents !== undefined ? { subagents } : {})
|
|
376
|
+
} catch (err) {
|
|
377
|
+
console.error(`[cron] load failed: ${err instanceof Error ? err.message : err}`)
|
|
378
|
+
return null
|
|
379
|
+
}
|
|
380
|
+
if (!result.ok) {
|
|
381
|
+
console.error(`[cron] failed to load cron.json: ${result.reason}`)
|
|
382
|
+
return null
|
|
383
|
+
}
|
|
384
|
+
const file: CronFile = result.file ?? { jobs: [] }
|
|
385
|
+
if (!result.file && !hasInternalJobs) return null
|
|
386
|
+
|
|
387
|
+
const onFire = (job: CronJob) => {
|
|
388
|
+
stream.publish({ target: { kind: 'cron', jobId: job.id }, payload: job })
|
|
389
|
+
}
|
|
390
|
+
const scheduler = createSchedulerFor({ cwd, file, onFire })
|
|
391
|
+
scheduler.start()
|
|
392
|
+
return scheduler
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function makeDefaultSchedulerFactory(internalJobs: () => CronJob[]): SchedulerFactory {
|
|
396
|
+
return ({ file, onFire }) => createScheduler({ jobs: [...file.jobs, ...internalJobs()], onFire })
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function mergeSubagents(pluginRegistry: PluginRegistry): {
|
|
400
|
+
registry: SubagentRegistry
|
|
401
|
+
pluginSubagentByShim: WeakMap<InternalSubagent<any>, PluginSubagentEntry>
|
|
402
|
+
pluginSubagentByName: Map<string, PluginSubagentEntry>
|
|
403
|
+
} {
|
|
404
|
+
const merged: Record<string, InternalSubagent<any>> = {}
|
|
405
|
+
const pluginSubagentByShim = new WeakMap<InternalSubagent<any>, PluginSubagentEntry>()
|
|
406
|
+
const pluginSubagentByName = new Map<string, PluginSubagentEntry>()
|
|
407
|
+
for (const reg of pluginRegistry.subagents) {
|
|
408
|
+
if (merged[reg.subagentName] !== undefined) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`plugin ${reg.pluginName}: subagent name "${reg.subagentName}" already registered (across plugins)`,
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
const shim = pluginSubagentShim(reg.subagent)
|
|
414
|
+
merged[reg.subagentName] = shim
|
|
415
|
+
const entry: PluginSubagentEntry = {
|
|
416
|
+
pluginName: reg.pluginName,
|
|
417
|
+
subagentName: reg.subagentName,
|
|
418
|
+
pluginSubagent: reg.subagent,
|
|
419
|
+
}
|
|
420
|
+
pluginSubagentByShim.set(shim, entry)
|
|
421
|
+
pluginSubagentByName.set(reg.subagentName, entry)
|
|
422
|
+
}
|
|
423
|
+
return { registry: merged, pluginSubagentByShim, pluginSubagentByName }
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function pluginSubagentShim(subagent: import('@/plugin').Subagent<any>): InternalSubagent<any> {
|
|
427
|
+
return {
|
|
428
|
+
systemPrompt: subagent.systemPrompt,
|
|
429
|
+
...(subagent.payloadSchema ? { payloadSchema: subagent.payloadSchema } : {}),
|
|
430
|
+
...(subagent.handler ? { handler: subagent.handler as InternalSubagent<any>['handler'] } : {}),
|
|
431
|
+
}
|
|
432
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Subagent as InternalSubagent, SubagentRegistry } from '@/agent/subagents'
|
|
2
|
+
import type { HookBus, MaterializedSkills, PluginRegistry, Subagent as PluginSubagent } from '@/plugin'
|
|
3
|
+
|
|
4
|
+
export type PluginSubagentEntry = {
|
|
5
|
+
pluginName: string
|
|
6
|
+
subagentName: string
|
|
7
|
+
pluginSubagent: PluginSubagent<any>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type PluginRuntimeState = {
|
|
11
|
+
registry: PluginRegistry
|
|
12
|
+
hooks: HookBus
|
|
13
|
+
subagents: SubagentRegistry
|
|
14
|
+
pluginSubagentByShim: WeakMap<InternalSubagent<any>, PluginSubagentEntry>
|
|
15
|
+
hasAnyPluginContent: boolean
|
|
16
|
+
loadedPlugins: { name: string; version: string | undefined; source: string }[]
|
|
17
|
+
materializedSkills: MaterializedSkills | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type PluginRuntime = {
|
|
21
|
+
get: () => PluginRuntimeState
|
|
22
|
+
swap: (next: PluginRuntimeState) => PluginRuntimeState
|
|
23
|
+
trackPendingDisposal: (skills: MaterializedSkills) => void
|
|
24
|
+
drainPendingDisposal: () => MaterializedSkills[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createPluginRuntime(initial: PluginRuntimeState): PluginRuntime {
|
|
28
|
+
let current = initial
|
|
29
|
+
const pendingDisposal: MaterializedSkills[] = []
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
get: () => current,
|
|
33
|
+
swap: (next) => {
|
|
34
|
+
const prev = current
|
|
35
|
+
current = next
|
|
36
|
+
return prev
|
|
37
|
+
},
|
|
38
|
+
trackPendingDisposal: (skills) => {
|
|
39
|
+
pendingDisposal.push(skills)
|
|
40
|
+
},
|
|
41
|
+
drainPendingDisposal: () => pendingDisposal.splice(0, pendingDisposal.length),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { BUNDLED_PLUGINS } from './bundled-plugins'
|
|
4
|
+
|
|
5
|
+
export function buildConfigSchemaWithBundledPlugins(coreSchema: z.ZodObject): z.ZodObject {
|
|
6
|
+
const pluginShape: Record<string, z.ZodType> = {}
|
|
7
|
+
for (const plugin of BUNDLED_PLUGINS) {
|
|
8
|
+
const schema = plugin.defined.configSchema
|
|
9
|
+
if (schema !== undefined) {
|
|
10
|
+
pluginShape[plugin.name] = schema as z.ZodType
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return coreSchema.extend(pluginShape)
|
|
14
|
+
}
|