typeclaw 0.1.5 → 0.2.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 +14 -12
- package/auth.schema.json +41 -0
- package/cron.schema.json +8 -0
- package/package.json +1 -1
- package/secrets.schema.json +41 -0
- package/src/agent/auth.ts +45 -22
- package/src/agent/index.ts +189 -19
- package/src/agent/multimodal/index.ts +12 -0
- package/src/agent/multimodal/look-at.ts +185 -0
- package/src/agent/multimodal/looker.ts +145 -0
- package/src/agent/plugin-tools.ts +30 -1
- package/src/agent/session-origin.ts +194 -46
- package/src/agent/subagents.ts +57 -1
- package/src/agent/system-prompt.ts +1 -1
- package/src/agent/tool-result-budget.ts +121 -0
- package/src/bundled-plugins/backup/index.ts +23 -8
- package/src/bundled-plugins/backup/runner.ts +22 -0
- package/src/bundled-plugins/memory/README.md +7 -4
- package/src/bundled-plugins/memory/append-tool.ts +87 -61
- package/src/bundled-plugins/memory/dreaming.ts +23 -9
- package/src/bundled-plugins/memory/find-entry-tool.ts +62 -0
- package/src/bundled-plugins/memory/fragment-parser.ts +19 -44
- package/src/bundled-plugins/memory/index.ts +91 -8
- package/src/bundled-plugins/memory/load-memory.ts +74 -34
- package/src/bundled-plugins/memory/memory-logger.ts +72 -29
- package/src/bundled-plugins/memory/migration.ts +276 -0
- package/src/bundled-plugins/memory/stream-events.ts +55 -0
- package/src/bundled-plugins/memory/stream-io.ts +63 -0
- package/src/bundled-plugins/memory/watermark.ts +48 -8
- package/src/bundled-plugins/security/index.ts +103 -10
- package/src/bundled-plugins/security/permissions.ts +12 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +51 -18
- package/src/bundled-plugins/tool-result-cap/README.md +9 -4
- package/src/bundled-plugins/tool-result-cap/cap-jsonl.ts +115 -0
- package/src/bundled-plugins/tool-result-cap/cap-result.ts +25 -13
- package/src/bundled-plugins/tool-result-cap/index.ts +16 -2
- package/src/channels/adapters/discord-bot-classify.ts +2 -6
- package/src/channels/adapters/discord-bot.ts +4 -45
- package/src/channels/adapters/kakaotalk-classify.ts +3 -7
- package/src/channels/adapters/kakaotalk.ts +28 -47
- package/src/channels/adapters/slack-bot-classify.ts +2 -6
- package/src/channels/adapters/slack-bot.ts +4 -50
- package/src/channels/adapters/telegram-bot-classify.ts +8 -10
- package/src/channels/adapters/telegram-bot.ts +3 -16
- package/src/channels/index.ts +3 -2
- package/src/channels/manager.ts +15 -1
- package/src/channels/persistence.ts +44 -10
- package/src/channels/router.ts +228 -19
- package/src/channels/schema.ts +6 -156
- package/src/cli/channel.ts +200 -4
- package/src/cli/compose-usage.ts +182 -0
- package/src/cli/compose.ts +33 -0
- package/src/cli/hostd.ts +49 -1
- package/src/cli/index.ts +4 -0
- package/src/cli/init.ts +799 -319
- package/src/cli/model.ts +244 -0
- package/src/cli/provider.ts +404 -0
- package/src/cli/reload.ts +6 -1
- package/src/cli/role.ts +156 -0
- package/src/cli/run.ts +3 -1
- package/src/cli/tui.ts +8 -1
- package/src/cli/usage-args.ts +47 -0
- package/src/cli/usage.ts +97 -0
- package/src/compose/index.ts +1 -0
- package/src/compose/usage.ts +65 -0
- package/src/config/config.ts +385 -12
- package/src/config/index.ts +7 -0
- package/src/config/models-mutation.ts +209 -0
- package/src/config/providers-mutation.ts +250 -0
- package/src/config/providers.ts +141 -2
- package/src/config/reloadable.ts +15 -4
- package/src/container/index.ts +5 -0
- package/src/container/require-running.ts +33 -0
- package/src/container/start.ts +39 -58
- package/src/cron/consumer.ts +22 -2
- package/src/cron/index.ts +45 -4
- package/src/cron/schema.ts +104 -0
- package/src/doctor/checks.ts +50 -33
- package/src/git/system-commit.ts +103 -0
- package/src/hostd/daemon.ts +16 -0
- package/src/hostd/kakao-renewal-manager.ts +223 -0
- package/src/hostd/paths.ts +7 -0
- package/src/init/dockerfile.ts +32 -6
- package/src/init/index.ts +190 -61
- package/src/init/kakaotalk-auth.ts +18 -1
- package/src/init/models-dev.ts +26 -1
- package/src/init/run-owner-claim.ts +77 -0
- package/src/permissions/builtins.ts +70 -0
- package/src/permissions/grant.ts +99 -0
- package/src/permissions/index.ts +29 -0
- package/src/permissions/match-rule.ts +305 -0
- package/src/permissions/permissions.ts +196 -0
- package/src/permissions/resolve.ts +80 -0
- package/src/permissions/schema.ts +79 -0
- package/src/plugin/context.ts +8 -4
- package/src/plugin/define.ts +2 -0
- package/src/plugin/index.ts +2 -0
- package/src/plugin/manager.ts +41 -0
- package/src/plugin/registry.ts +9 -0
- package/src/plugin/types.ts +35 -1
- package/src/role-claim/client.ts +182 -0
- package/src/role-claim/code.ts +53 -0
- package/src/role-claim/controller.ts +194 -0
- package/src/role-claim/index.ts +19 -0
- package/src/role-claim/match-rule.ts +43 -0
- package/src/role-claim/pending.ts +100 -0
- package/src/run/channel-session-factory.ts +76 -5
- package/src/run/index.ts +55 -6
- package/src/secrets/encryption.ts +116 -0
- package/src/secrets/kakao-renewal.ts +248 -0
- package/src/secrets/kakao-store.ts +66 -7
- package/src/secrets/keys.ts +173 -0
- package/src/secrets/schema.ts +23 -0
- package/src/secrets/storage.ts +68 -0
- package/src/server/index.ts +122 -11
- package/src/shared/index.ts +4 -0
- package/src/shared/protocol.ts +27 -0
- package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +3 -3
- package/src/skills/typeclaw-config/SKILL.md +38 -64
- package/src/skills/typeclaw-memory/SKILL.md +1 -1
- package/src/skills/typeclaw-permissions/SKILL.md +166 -0
- package/src/stream/types.ts +7 -1
- package/src/usage/aggregate.ts +117 -0
- package/src/usage/format.ts +30 -0
- package/src/usage/index.ts +68 -0
- package/src/usage/report.ts +354 -0
- package/src/usage/scan.ts +186 -0
- package/typeclaw.schema.json +57 -45
package/README.md
CHANGED
|
@@ -68,18 +68,20 @@ That's it. The agent is now alive, listening on a websocket, ready to receive pr
|
|
|
68
68
|
|
|
69
69
|
## CLI
|
|
70
70
|
|
|
71
|
-
| Command
|
|
72
|
-
|
|
|
73
|
-
| `typeclaw init`
|
|
74
|
-
| `typeclaw start`
|
|
75
|
-
| `typeclaw stop`
|
|
76
|
-
| `typeclaw restart`
|
|
77
|
-
| `typeclaw status`
|
|
78
|
-
| `typeclaw logs`
|
|
79
|
-
| `typeclaw tui`
|
|
80
|
-
| `typeclaw shell`
|
|
81
|
-
| `typeclaw reload`
|
|
82
|
-
| `typeclaw compose`
|
|
71
|
+
| Command | Purpose |
|
|
72
|
+
| ----------------------------------- | ---------------------------------------------------------------------------------- |
|
|
73
|
+
| `typeclaw init` | Scaffold a new agent folder |
|
|
74
|
+
| `typeclaw start` | Build and run the container |
|
|
75
|
+
| `typeclaw stop` | Stop the container |
|
|
76
|
+
| `typeclaw restart` | `stop` then `start` |
|
|
77
|
+
| `typeclaw status` | Show container + daemon registration state |
|
|
78
|
+
| `typeclaw logs` | Stream container stdout/stderr with local timestamps; `-f` to follow |
|
|
79
|
+
| `typeclaw tui` | Attach a terminal UI over the agent's websocket |
|
|
80
|
+
| `typeclaw shell` | Open a shell inside the running container |
|
|
81
|
+
| `typeclaw reload` | Push a live config reload to the running agent |
|
|
82
|
+
| `typeclaw compose` | Orchestrate multiple agents |
|
|
83
|
+
| `typeclaw channel add <kind>` | Wire a new channel adapter (Slack, Discord, Telegram, KakaoTalk) |
|
|
84
|
+
| `typeclaw channel reauth kakaotalk` | Re-authenticate KakaoTalk after a stale-token 401 or to rotate the stored password |
|
|
83
85
|
|
|
84
86
|
## Configuration
|
|
85
87
|
|
package/auth.schema.json
CHANGED
|
@@ -233,6 +233,47 @@
|
|
|
233
233
|
},
|
|
234
234
|
"updated_at": {
|
|
235
235
|
"type": "string"
|
|
236
|
+
},
|
|
237
|
+
"email": {
|
|
238
|
+
"type": "string"
|
|
239
|
+
},
|
|
240
|
+
"encryptedPassword": {
|
|
241
|
+
"type": "object",
|
|
242
|
+
"properties": {
|
|
243
|
+
"v": {
|
|
244
|
+
"type": "number",
|
|
245
|
+
"const": 1
|
|
246
|
+
},
|
|
247
|
+
"alg": {
|
|
248
|
+
"type": "string",
|
|
249
|
+
"const": "AES-256-GCM"
|
|
250
|
+
},
|
|
251
|
+
"kid": {
|
|
252
|
+
"type": "string"
|
|
253
|
+
},
|
|
254
|
+
"iv": {
|
|
255
|
+
"type": "string"
|
|
256
|
+
},
|
|
257
|
+
"ciphertext": {
|
|
258
|
+
"type": "string"
|
|
259
|
+
},
|
|
260
|
+
"authTag": {
|
|
261
|
+
"type": "string"
|
|
262
|
+
},
|
|
263
|
+
"createdAt": {
|
|
264
|
+
"type": "string"
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
"required": [
|
|
268
|
+
"v",
|
|
269
|
+
"alg",
|
|
270
|
+
"kid",
|
|
271
|
+
"iv",
|
|
272
|
+
"ciphertext",
|
|
273
|
+
"authTag",
|
|
274
|
+
"createdAt"
|
|
275
|
+
],
|
|
276
|
+
"additionalProperties": false
|
|
236
277
|
}
|
|
237
278
|
},
|
|
238
279
|
"required": [
|
package/cron.schema.json
CHANGED
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
"timezone": {
|
|
30
30
|
"type": "string"
|
|
31
31
|
},
|
|
32
|
+
"scheduledByRole": {
|
|
33
|
+
"type": "string"
|
|
34
|
+
},
|
|
35
|
+
"scheduledByOrigin": {},
|
|
32
36
|
"kind": {
|
|
33
37
|
"type": "string",
|
|
34
38
|
"const": "prompt"
|
|
@@ -69,6 +73,10 @@
|
|
|
69
73
|
"timezone": {
|
|
70
74
|
"type": "string"
|
|
71
75
|
},
|
|
76
|
+
"scheduledByRole": {
|
|
77
|
+
"type": "string"
|
|
78
|
+
},
|
|
79
|
+
"scheduledByOrigin": {},
|
|
72
80
|
"kind": {
|
|
73
81
|
"type": "string",
|
|
74
82
|
"const": "exec"
|
package/package.json
CHANGED
package/secrets.schema.json
CHANGED
|
@@ -233,6 +233,47 @@
|
|
|
233
233
|
},
|
|
234
234
|
"updated_at": {
|
|
235
235
|
"type": "string"
|
|
236
|
+
},
|
|
237
|
+
"email": {
|
|
238
|
+
"type": "string"
|
|
239
|
+
},
|
|
240
|
+
"encryptedPassword": {
|
|
241
|
+
"type": "object",
|
|
242
|
+
"properties": {
|
|
243
|
+
"v": {
|
|
244
|
+
"type": "number",
|
|
245
|
+
"const": 1
|
|
246
|
+
},
|
|
247
|
+
"alg": {
|
|
248
|
+
"type": "string",
|
|
249
|
+
"const": "AES-256-GCM"
|
|
250
|
+
},
|
|
251
|
+
"kid": {
|
|
252
|
+
"type": "string"
|
|
253
|
+
},
|
|
254
|
+
"iv": {
|
|
255
|
+
"type": "string"
|
|
256
|
+
},
|
|
257
|
+
"ciphertext": {
|
|
258
|
+
"type": "string"
|
|
259
|
+
},
|
|
260
|
+
"authTag": {
|
|
261
|
+
"type": "string"
|
|
262
|
+
},
|
|
263
|
+
"createdAt": {
|
|
264
|
+
"type": "string"
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
"required": [
|
|
268
|
+
"v",
|
|
269
|
+
"alg",
|
|
270
|
+
"kid",
|
|
271
|
+
"iv",
|
|
272
|
+
"ciphertext",
|
|
273
|
+
"authTag",
|
|
274
|
+
"createdAt"
|
|
275
|
+
],
|
|
276
|
+
"additionalProperties": false
|
|
236
277
|
}
|
|
237
278
|
},
|
|
238
279
|
"required": [
|
package/src/agent/auth.ts
CHANGED
|
@@ -23,12 +23,18 @@ function secretsJsonPath(): string {
|
|
|
23
23
|
return join(process.cwd(), 'secrets.json')
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
// Per-provider cache. Sessions that use a profile mapped to provider X share
|
|
27
|
+
// a single AuthStorage + ModelRegistry for that provider; first use of a new
|
|
28
|
+
// provider lazily resolves its credentials. This replaces the singleton
|
|
29
|
+
// `getAuth()` from before multi-model — the singleton couldn't represent
|
|
30
|
+
// "auth for `default` is OpenAI, auth for `vision` is Fireworks" without
|
|
31
|
+
// constructing both at boot.
|
|
32
|
+
const cached = new Map<KnownProviderId, Auth>()
|
|
33
|
+
|
|
34
|
+
export function getAuthFor(providerId: KnownProviderId): Auth {
|
|
35
|
+
const existing = cached.get(providerId)
|
|
36
|
+
if (existing) return existing
|
|
27
37
|
|
|
28
|
-
export function getAuth(): Auth {
|
|
29
|
-
if (cached) return cached
|
|
30
|
-
|
|
31
|
-
const providerId = providerForModelRef(getConfig().model)
|
|
32
38
|
const provider = KNOWN_PROVIDERS[providerId]
|
|
33
39
|
|
|
34
40
|
if (process.env.NODE_ENV === 'test' && !hasAnyCredentialInEnv(provider.apiKeyEnv)) {
|
|
@@ -37,8 +43,9 @@ export function getAuth(): Auth {
|
|
|
37
43
|
authStorage.setRuntimeApiKey(provider.id, TEST_DUMMY_API_KEY)
|
|
38
44
|
}
|
|
39
45
|
const modelRegistry = ModelRegistry.create(authStorage)
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
const auth = { authStorage, modelRegistry }
|
|
47
|
+
cached.set(providerId, auth)
|
|
48
|
+
return auth
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
const authStorage = createSecretsStoreForAgent(secretsJsonPath())
|
|
@@ -53,16 +60,11 @@ export function getAuth(): Auth {
|
|
|
53
60
|
// is set. OAuth credentials in the file still take precedence on read
|
|
54
61
|
// because AuthStorage's hasAuth checks runtimeOverrides first only for
|
|
55
62
|
// api-key-shaped credentials — OAuth on disk wins on its own.
|
|
56
|
-
//
|
|
57
|
-
// The previous code branch that wrote the env value into secrets.json and
|
|
58
|
-
// stripped the matching `.env` line has been removed. Env values stay in
|
|
59
|
-
// env; the file stays user-owned. See src/secrets/hydrate.ts for the same
|
|
60
|
-
// policy on the channels side.
|
|
61
63
|
if (supportsApiKey(provider) && provider.apiKeyEnv) {
|
|
62
64
|
const envKey = process.env[provider.apiKeyEnv]
|
|
63
65
|
if (envKey !== undefined && envKey !== '') {
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
+
const existingCred = authStorage.get(provider.id)
|
|
67
|
+
if (existingCred === undefined || existingCred.type === 'api_key') {
|
|
66
68
|
authStorage.setRuntimeApiKey(provider.id, envKey)
|
|
67
69
|
}
|
|
68
70
|
}
|
|
@@ -74,12 +76,20 @@ export function getAuth(): Auth {
|
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
const modelRegistry = ModelRegistry.create(authStorage)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
const auth = { authStorage, modelRegistry }
|
|
80
|
+
cached.set(providerId, auth)
|
|
81
|
+
return auth
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Back-compat shim for callers that still want the `default` profile's auth
|
|
85
|
+
// (the main session path). Equivalent to `getAuthFor(provider-of-default)`.
|
|
86
|
+
export function getAuth(): Auth {
|
|
87
|
+
const defaultRef = getConfig().models.default
|
|
88
|
+
return getAuthFor(providerForModelRef(defaultRef))
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
export function resetAuthForTesting(): void {
|
|
82
|
-
cached
|
|
92
|
+
cached.clear()
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
function hasAnyCredentialInEnv(apiKeyEnv: string | null): boolean {
|
|
@@ -88,19 +98,32 @@ function hasAnyCredentialInEnv(apiKeyEnv: string | null): boolean {
|
|
|
88
98
|
|
|
89
99
|
function missingCredentialMessage(providerId: KnownProviderId): string {
|
|
90
100
|
const provider = KNOWN_PROVIDERS[providerId]
|
|
91
|
-
const
|
|
92
|
-
const
|
|
101
|
+
const defaultRef = getConfig().models.default
|
|
102
|
+
const defaultProviderId = providerForModelRef(defaultRef)
|
|
103
|
+
// For the `default` profile, name the model in the error message (matches
|
|
104
|
+
// pre-multi-model behavior). For any other profile, the user is mixing
|
|
105
|
+
// providers across profiles and the error must name the failing provider
|
|
106
|
+
// without claiming it's tied to the `default` model.
|
|
107
|
+
const isDefault = defaultProviderId === providerId
|
|
108
|
+
const ref = isDefault ? defaultRef : null
|
|
93
109
|
const modelName =
|
|
94
|
-
|
|
110
|
+
ref !== null
|
|
111
|
+
? ((provider.models as Record<string, { name: string }>)[ref.slice(ref.indexOf('/') + 1)]?.name ??
|
|
112
|
+
ref.slice(ref.indexOf('/') + 1))
|
|
113
|
+
: null
|
|
95
114
|
|
|
96
115
|
const oauthOnly = supportsOAuth(provider) && !supportsApiKey(provider)
|
|
97
116
|
const apiKeyOnly = supportsApiKey(provider) && !supportsOAuth(provider)
|
|
98
117
|
|
|
99
118
|
if (oauthOnly) {
|
|
100
|
-
return
|
|
119
|
+
return modelName
|
|
120
|
+
? `No credentials for ${provider.name}. Run \`typeclaw init\` and pick "OAuth" to log in to ${modelName}.`
|
|
121
|
+
: `No credentials for ${provider.name} (referenced by a non-default profile). Run \`typeclaw init\` and pick "OAuth" to log in.`
|
|
101
122
|
}
|
|
102
123
|
if (apiKeyOnly && provider.apiKeyEnv) {
|
|
103
|
-
return
|
|
124
|
+
return modelName
|
|
125
|
+
? `Set ${provider.apiKeyEnv} in .env (or secrets.json#providers.${provider.id}.key.value) to use ${modelName} via ${provider.name}.`
|
|
126
|
+
: `Set ${provider.apiKeyEnv} in .env (or secrets.json#providers.${provider.id}.key.value) to use ${provider.name} (referenced by a non-default profile).`
|
|
104
127
|
}
|
|
105
128
|
return `No credentials for ${provider.name}. Either set ${provider.apiKeyEnv ?? '<api-key-env>'} in .env or run \`typeclaw init\` and pick "OAuth".`
|
|
106
129
|
}
|
package/src/agent/index.ts
CHANGED
|
@@ -5,8 +5,11 @@ import { fileURLToPath } from 'node:url'
|
|
|
5
5
|
import { createAgentSession, DefaultResourceLoader, SessionManager } from '@mariozechner/pi-coding-agent'
|
|
6
6
|
import type { AgentSession, ToolDefinition } from '@mariozechner/pi-coding-agent'
|
|
7
7
|
|
|
8
|
+
import { loadMemory } from '@/bundled-plugins/memory/load-memory'
|
|
8
9
|
import type { ChannelRouter } from '@/channels/router'
|
|
9
|
-
import { getConfig, resolveModel } from '@/config'
|
|
10
|
+
import { getConfig, resolveModel, resolveProfile } from '@/config'
|
|
11
|
+
import { providerForModelRef } from '@/config/providers'
|
|
12
|
+
import type { PermissionService } from '@/permissions'
|
|
10
13
|
import type {
|
|
11
14
|
BuiltinToolRef,
|
|
12
15
|
HookBus,
|
|
@@ -19,14 +22,21 @@ import { materializeSkills } from '@/plugin'
|
|
|
19
22
|
import type { ReloadRegistry } from '@/reload'
|
|
20
23
|
import type { Stream } from '@/stream'
|
|
21
24
|
|
|
22
|
-
import {
|
|
25
|
+
import { getAuthFor } from './auth'
|
|
23
26
|
import { createCompactionSettingsManager } from './compaction'
|
|
24
27
|
import { renderGitNudge } from './git-nudge'
|
|
28
|
+
import { lookAtTool } from './multimodal'
|
|
25
29
|
import { resolveBuiltinToolRefs, wrapPluginTool, wrapSystemAgentTool, wrapSystemTool } from './plugin-tools'
|
|
26
30
|
import { createReloadTool } from './reload-tool'
|
|
27
31
|
import { loadSelf } from './self'
|
|
28
|
-
import { renderSessionOrigin, type SessionOrigin } from './session-origin'
|
|
32
|
+
import { renderSessionOrigin, type SessionOrigin, type SessionRoleContext } from './session-origin'
|
|
29
33
|
import { DEFAULT_SYSTEM_PROMPT } from './system-prompt'
|
|
34
|
+
import {
|
|
35
|
+
createBudgetState,
|
|
36
|
+
type ToolResultBudget,
|
|
37
|
+
wrapAgentToolWithBudget,
|
|
38
|
+
wrapToolDefinitionWithBudget,
|
|
39
|
+
} from './tool-result-budget'
|
|
30
40
|
import { createChannelFetchAttachmentTool } from './tools/channel-fetch-attachment'
|
|
31
41
|
import { createChannelHistoryTool } from './tools/channel-history'
|
|
32
42
|
import { createChannelReplyTool } from './tools/channel-reply'
|
|
@@ -56,6 +66,15 @@ export type PluginSubagentSelection = {
|
|
|
56
66
|
toolNamePrefix: string
|
|
57
67
|
}
|
|
58
68
|
|
|
69
|
+
// Mutable holder for the live session origin. Pass this when the origin
|
|
70
|
+
// must be updated turn-by-turn after session creation (channel sessions
|
|
71
|
+
// whose `lastInboundAuthorId` changes with each inbound message). Tool
|
|
72
|
+
// wrappers read `.current` at execute time, not at wrap time, so the
|
|
73
|
+
// `tool.before` event carries the per-turn actor identity rather than the
|
|
74
|
+
// stale session-creation snapshot. Sessions that never mutate origin
|
|
75
|
+
// (TUI, cron, subagent) can omit it and pass `origin` instead.
|
|
76
|
+
export type SessionOriginRef = { current: SessionOrigin | undefined }
|
|
77
|
+
|
|
59
78
|
export type CreateSessionOptions = {
|
|
60
79
|
reloadRegistry?: ReloadRegistry
|
|
61
80
|
sessionManager?: SessionManager
|
|
@@ -68,6 +87,13 @@ export type CreateSessionOptions = {
|
|
|
68
87
|
// Rendered into the system prompt so the agent knows who's listening, where
|
|
69
88
|
// its output goes, and what to pass to channel_send.
|
|
70
89
|
origin?: SessionOrigin
|
|
90
|
+
// Live origin holder. When provided, the tool wrappers read this at execute
|
|
91
|
+
// time so `tool.before` events see the current-turn origin. Caller is
|
|
92
|
+
// responsible for keeping `.current` up to date. If both `origin` and
|
|
93
|
+
// `originRef` are passed, the ref wins for tool stamping; the static
|
|
94
|
+
// `origin` still drives the initial system-prompt rendering and channel
|
|
95
|
+
// tool addressing (those are only valid at session-creation time).
|
|
96
|
+
originRef?: SessionOriginRef
|
|
71
97
|
tools?: AgentSessionTools
|
|
72
98
|
customTools?: ToolDefinition[]
|
|
73
99
|
plugins?: PluginSessionWiring
|
|
@@ -78,6 +104,41 @@ export type CreateSessionOptions = {
|
|
|
78
104
|
// Enables the `restart` tool. Set when the agent is running inside a
|
|
79
105
|
// typeclaw-managed container. Read from TYPECLAW_CONTAINER_NAME at the call site.
|
|
80
106
|
containerName?: string
|
|
107
|
+
// The permission service the runtime resolved at boot. When provided, the
|
|
108
|
+
// resolved role and permission list for `options.origin` are rendered into
|
|
109
|
+
// the system prompt under `## Your role in this session`. The block is
|
|
110
|
+
// emitted for channel/cron/subagent sessions, and for TUI sessions only
|
|
111
|
+
// when the resolved role is not the built-in `owner` (because TUI
|
|
112
|
+
// resolving to `owner` is the common case and we save tokens on every
|
|
113
|
+
// interactive session). Omitting `permissions` falls back to the previous
|
|
114
|
+
// behavior (no role annotation), which is what tests and stand-alone
|
|
115
|
+
// callers want.
|
|
116
|
+
//
|
|
117
|
+
// The role rendered here is a session-creation snapshot. Channel sessions
|
|
118
|
+
// re-resolve per-turn through `originRef` for tool gating, but the system
|
|
119
|
+
// prompt is not regenerated; see `typeclaw-permissions` skill for how the
|
|
120
|
+
// agent should interpret the snapshot on later turns.
|
|
121
|
+
permissions?: PermissionService
|
|
122
|
+
// Model profile name. Resolved against `config.models` to pick the concrete
|
|
123
|
+
// model ref this session binds to. Unknown profile names fall back to
|
|
124
|
+
// `default` with a one-time console warning. Omitted → `default`. Threaded
|
|
125
|
+
// through from the caller (subagent declarations, future per-spawn tool
|
|
126
|
+
// overrides) so different sessions on the same agent can run different
|
|
127
|
+
// models without per-session config edits.
|
|
128
|
+
profile?: string
|
|
129
|
+
// Defensive ceiling on cumulative bytes of tool-result text per session,
|
|
130
|
+
// applied to the named tools only. See `src/agent/tool-result-budget.ts`
|
|
131
|
+
// for the rationale. Intended for subagents that read large files
|
|
132
|
+
// (memory-logger, dreaming); leaving this undefined disables the budget
|
|
133
|
+
// entirely, which is the right default for TUI / channel / plugin-tool
|
|
134
|
+
// sessions where the human (or hooks) bound tool-result size.
|
|
135
|
+
toolResultBudget?: ToolResultBudget
|
|
136
|
+
// Optional override for the message returned to the agent once
|
|
137
|
+
// `toolResultBudget` is exhausted. Subagents whose recovery path differs
|
|
138
|
+
// from the default ("advance the watermark from a recent id you have
|
|
139
|
+
// already seen") provide their own here. See `ToolResultBudget` for the
|
|
140
|
+
// shared shape.
|
|
141
|
+
toolResultBudgetMessage?: ToolResultBudget['exhaustedMessage']
|
|
81
142
|
}
|
|
82
143
|
|
|
83
144
|
export type CreateSessionResult = {
|
|
@@ -91,7 +152,11 @@ export async function createSession(options: CreateSessionOptions = {}): Promise
|
|
|
91
152
|
}
|
|
92
153
|
|
|
93
154
|
export async function createSessionWithDispose(options: CreateSessionOptions = {}): Promise<CreateSessionResult> {
|
|
94
|
-
const
|
|
155
|
+
const resolved = resolveProfile(getConfig().models, options.profile)
|
|
156
|
+
if (resolved.fellBackToDefault && options.profile !== undefined && options.profile !== 'default') {
|
|
157
|
+
warnProfileFallbackOnce(options.profile, resolved.ref)
|
|
158
|
+
}
|
|
159
|
+
const { authStorage, modelRegistry } = getAuthFor(providerForModelRef(resolved.ref))
|
|
95
160
|
|
|
96
161
|
const materializedSkills =
|
|
97
162
|
options.plugins && options.plugins.registry.skills.length > 0
|
|
@@ -106,23 +171,46 @@ export async function createSessionWithDispose(options: CreateSessionOptions = {
|
|
|
106
171
|
|
|
107
172
|
const resourceLoader =
|
|
108
173
|
options.systemPromptOverride !== undefined
|
|
109
|
-
? await createOverrideResourceLoader(options.systemPromptOverride, options.origin)
|
|
174
|
+
? await createOverrideResourceLoader(options.systemPromptOverride, options.origin, options.permissions)
|
|
110
175
|
: await createResourceLoader({
|
|
111
176
|
...(options.plugins ? { plugins: options.plugins, materializedSkills } : {}),
|
|
112
177
|
...(options.origin ? { origin: options.origin } : {}),
|
|
178
|
+
...(options.permissions ? { permissions: options.permissions } : {}),
|
|
113
179
|
})
|
|
114
180
|
|
|
181
|
+
const getOrigin: () => SessionOrigin | undefined =
|
|
182
|
+
options.originRef !== undefined ? () => options.originRef!.current : () => options.origin
|
|
183
|
+
|
|
115
184
|
const subagentBuiltinTools = options.pluginSubagent?.toolRefs
|
|
116
185
|
? resolveBuiltinToolRefs(options.pluginSubagent.toolRefs)
|
|
117
186
|
: undefined
|
|
118
187
|
const pluginCustomTools = options.pluginSubagent
|
|
119
|
-
? wrapSubagentCustomTools(options.pluginSubagent, options.plugins)
|
|
120
|
-
: wrapRegistryTools(options.plugins)
|
|
188
|
+
? wrapSubagentCustomTools(options.pluginSubagent, options.plugins, getOrigin)
|
|
189
|
+
: wrapRegistryTools(options.plugins, getOrigin)
|
|
190
|
+
|
|
191
|
+
// Per-run budget state for the tool-result byte ceiling. Allocated once per
|
|
192
|
+
// session creation and threaded into every wrapped tool so they share the
|
|
193
|
+
// same counter. Only used when the session declares a budget; the wrappers
|
|
194
|
+
// pass non-listed tools through unchanged, so the counter stays at zero for
|
|
195
|
+
// sessions without a budget configured.
|
|
196
|
+
const sessionBudget: ToolResultBudget | undefined = options.toolResultBudget
|
|
197
|
+
? options.toolResultBudgetMessage !== undefined
|
|
198
|
+
? { ...options.toolResultBudget, exhaustedMessage: options.toolResultBudgetMessage }
|
|
199
|
+
: options.toolResultBudget
|
|
200
|
+
: undefined
|
|
201
|
+
const sessionBudgetState = sessionBudget ? createBudgetState() : undefined
|
|
121
202
|
|
|
122
|
-
const
|
|
203
|
+
const hookWrappedTools = wrapSystemAgentTools(
|
|
123
204
|
options.tools ?? (subagentBuiltinTools as AgentSessionTools | undefined),
|
|
124
205
|
options.plugins,
|
|
206
|
+
getOrigin,
|
|
125
207
|
)
|
|
208
|
+
const tools =
|
|
209
|
+
sessionBudget && sessionBudgetState && hookWrappedTools
|
|
210
|
+
? (hookWrappedTools.map((t) =>
|
|
211
|
+
wrapAgentToolWithBudget(t, sessionBudget, sessionBudgetState),
|
|
212
|
+
) as typeof hookWrappedTools)
|
|
213
|
+
: hookWrappedTools
|
|
126
214
|
|
|
127
215
|
// Hoisted above tool construction so the restart tool can be wired with the
|
|
128
216
|
// session's stable identity (sessionManager.getSessionId()). Subscribers use
|
|
@@ -138,6 +226,7 @@ export async function createSessionWithDispose(options: CreateSessionOptions = {
|
|
|
138
226
|
: [
|
|
139
227
|
websearchTool,
|
|
140
228
|
webfetchTool,
|
|
229
|
+
lookAtTool,
|
|
141
230
|
...(options.reloadRegistry ? [createReloadTool({ registry: options.reloadRegistry })] : []),
|
|
142
231
|
...(options.stream ? [createStreamSnapshotTool({ stream: options.stream })] : []),
|
|
143
232
|
...buildChannelTools(options.channelRouter, options.origin),
|
|
@@ -151,9 +240,13 @@ export async function createSessionWithDispose(options: CreateSessionOptions = {
|
|
|
151
240
|
]
|
|
152
241
|
: []),
|
|
153
242
|
]
|
|
154
|
-
const
|
|
243
|
+
const customToolsPreBudget = [...wrapSystemTools(customSystemTools, options.plugins, getOrigin), ...pluginCustomTools]
|
|
244
|
+
const customTools =
|
|
245
|
+
sessionBudget && sessionBudgetState
|
|
246
|
+
? customToolsPreBudget.map((t) => wrapToolDefinitionWithBudget(t, sessionBudget, sessionBudgetState))
|
|
247
|
+
: customToolsPreBudget
|
|
155
248
|
|
|
156
|
-
const model = resolveModel(
|
|
249
|
+
const model = resolveModel(resolved.ref)
|
|
157
250
|
const { session } = await createAgentSession({
|
|
158
251
|
model,
|
|
159
252
|
sessionManager,
|
|
@@ -294,7 +387,10 @@ export function buildChannelTools(
|
|
|
294
387
|
return tools
|
|
295
388
|
}
|
|
296
389
|
|
|
297
|
-
function wrapRegistryTools(
|
|
390
|
+
function wrapRegistryTools(
|
|
391
|
+
plugins: PluginSessionWiring | undefined,
|
|
392
|
+
getOrigin: () => SessionOrigin | undefined,
|
|
393
|
+
): ToolDefinition[] {
|
|
298
394
|
if (!plugins) return []
|
|
299
395
|
return plugins.registry.tools.map((t: PluginRegisteredTool) =>
|
|
300
396
|
wrapPluginTool(t.tool, {
|
|
@@ -304,6 +400,7 @@ function wrapRegistryTools(plugins: PluginSessionWiring | undefined): ToolDefini
|
|
|
304
400
|
sessionId: plugins.sessionId,
|
|
305
401
|
logger: t.logger,
|
|
306
402
|
hooks: plugins.hooks,
|
|
403
|
+
getOrigin,
|
|
307
404
|
}),
|
|
308
405
|
)
|
|
309
406
|
}
|
|
@@ -311,6 +408,7 @@ function wrapRegistryTools(plugins: PluginSessionWiring | undefined): ToolDefini
|
|
|
311
408
|
function wrapSystemAgentTools(
|
|
312
409
|
tools: AgentSessionTools | undefined,
|
|
313
410
|
plugins: PluginSessionWiring | undefined,
|
|
411
|
+
getOrigin: () => SessionOrigin | undefined,
|
|
314
412
|
): AgentSessionTools | undefined {
|
|
315
413
|
if (!tools || !hasToolHooks(plugins)) return tools
|
|
316
414
|
return tools.map((tool) =>
|
|
@@ -318,17 +416,23 @@ function wrapSystemAgentTools(
|
|
|
318
416
|
agentDir: plugins.agentDir,
|
|
319
417
|
sessionId: plugins.sessionId,
|
|
320
418
|
hooks: plugins.hooks,
|
|
419
|
+
getOrigin,
|
|
321
420
|
}),
|
|
322
421
|
)
|
|
323
422
|
}
|
|
324
423
|
|
|
325
|
-
function wrapSystemTools(
|
|
424
|
+
function wrapSystemTools(
|
|
425
|
+
tools: ToolDefinition[],
|
|
426
|
+
plugins: PluginSessionWiring | undefined,
|
|
427
|
+
getOrigin: () => SessionOrigin | undefined,
|
|
428
|
+
): ToolDefinition[] {
|
|
326
429
|
if (!hasToolHooks(plugins)) return tools
|
|
327
430
|
return tools.map((tool) =>
|
|
328
431
|
wrapSystemTool(tool, {
|
|
329
432
|
agentDir: plugins.agentDir,
|
|
330
433
|
sessionId: plugins.sessionId,
|
|
331
434
|
hooks: plugins.hooks,
|
|
435
|
+
getOrigin,
|
|
332
436
|
}),
|
|
333
437
|
)
|
|
334
438
|
}
|
|
@@ -341,6 +445,7 @@ function hasToolHooks(plugins: PluginSessionWiring | undefined): plugins is Plug
|
|
|
341
445
|
function wrapSubagentCustomTools(
|
|
342
446
|
selection: PluginSubagentSelection,
|
|
343
447
|
plugins: PluginSessionWiring | undefined,
|
|
448
|
+
getOrigin: () => SessionOrigin | undefined,
|
|
344
449
|
): ToolDefinition[] {
|
|
345
450
|
if (!selection.customTools || !plugins) return []
|
|
346
451
|
const logger = makePluginLogger(selection.pluginName)
|
|
@@ -352,6 +457,7 @@ function wrapSubagentCustomTools(
|
|
|
352
457
|
sessionId: plugins.sessionId,
|
|
353
458
|
logger,
|
|
354
459
|
hooks: plugins.hooks,
|
|
460
|
+
getOrigin,
|
|
355
461
|
}),
|
|
356
462
|
)
|
|
357
463
|
}
|
|
@@ -368,9 +474,11 @@ function makePluginLogger(pluginName: string) {
|
|
|
368
474
|
export async function createOverrideResourceLoader(
|
|
369
475
|
systemPrompt: string,
|
|
370
476
|
origin?: SessionOrigin,
|
|
477
|
+
permissions?: PermissionService,
|
|
371
478
|
): Promise<DefaultResourceLoader> {
|
|
479
|
+
const finalPrompt = withOrigin(systemPrompt, origin, permissions)
|
|
372
480
|
const loader = new DefaultResourceLoader({
|
|
373
|
-
systemPromptOverride: () =>
|
|
481
|
+
systemPromptOverride: () => finalPrompt,
|
|
374
482
|
appendSystemPromptOverride: () => [],
|
|
375
483
|
})
|
|
376
484
|
await loader.reload()
|
|
@@ -382,6 +490,7 @@ export type CreateResourceLoaderOptions = {
|
|
|
382
490
|
plugins?: PluginSessionWiring
|
|
383
491
|
materializedSkills?: MaterializedSkills | null
|
|
384
492
|
origin?: SessionOrigin
|
|
493
|
+
permissions?: PermissionService
|
|
385
494
|
}
|
|
386
495
|
|
|
387
496
|
export async function createResourceLoader(options: CreateResourceLoaderOptions = {}): Promise<DefaultResourceLoader> {
|
|
@@ -395,14 +504,32 @@ export async function createResourceLoader(options: CreateResourceLoaderOptions
|
|
|
395
504
|
systemPrompt = event.prompt
|
|
396
505
|
}
|
|
397
506
|
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
507
|
+
// Cache-suffix ordering: least-volatile sections first, most-volatile last.
|
|
508
|
+
// This minimises the number of cached prompt bytes invalidated when a
|
|
509
|
+
// section changes (the provider's prompt cache hits up to the first byte
|
|
510
|
+
// that differs).
|
|
511
|
+
//
|
|
512
|
+
// 1. origin block — stable across all sessions of the same kind.
|
|
513
|
+
// 2. gitNudge — rare changes; agent folders force-commit sessions/ and
|
|
514
|
+
// memory/ after every turn, so the dirty-files list is empty most of
|
|
515
|
+
// the time.
|
|
516
|
+
// 3. memorySection — most volatile: MEMORY.md grows on every dream cycle
|
|
517
|
+
// and memory/yyyy-MM-dd.md grows after every channel turn that triggers
|
|
518
|
+
// memory-logger. Pinning it to the end keeps everything above it
|
|
519
|
+
// cacheable across session resurrections.
|
|
520
|
+
systemPrompt = withOrigin(systemPrompt, options.origin, options.permissions)
|
|
521
|
+
|
|
401
522
|
const gitNudge = await renderGitNudge(agentDir)
|
|
402
523
|
if (gitNudge !== '') {
|
|
403
524
|
systemPrompt = `${systemPrompt}\n\n${gitNudge}`
|
|
404
525
|
}
|
|
405
526
|
|
|
527
|
+
const memorySection = await loadMemory(agentDir, {
|
|
528
|
+
...(options.origin !== undefined ? { origin: options.origin } : {}),
|
|
529
|
+
...(options.plugins?.sessionId !== undefined ? { currentSessionId: options.plugins.sessionId } : {}),
|
|
530
|
+
})
|
|
531
|
+
systemPrompt = `${systemPrompt}\n\n${memorySection}`
|
|
532
|
+
|
|
406
533
|
const additionalSkillPaths = [getBundledSkillsDir()]
|
|
407
534
|
// pi-coding-agent's DefaultResourceLoader auto-discovers <agentDir>/skills/
|
|
408
535
|
// but not <agentDir>/.agents/skills/. We do not scaffold <agentDir>/skills/
|
|
@@ -433,7 +560,7 @@ export async function createResourceLoader(options: CreateResourceLoaderOptions
|
|
|
433
560
|
}
|
|
434
561
|
|
|
435
562
|
const loader = new DefaultResourceLoader({
|
|
436
|
-
systemPromptOverride: () =>
|
|
563
|
+
systemPromptOverride: () => systemPrompt,
|
|
437
564
|
appendSystemPromptOverride: () => [],
|
|
438
565
|
additionalSkillPaths,
|
|
439
566
|
})
|
|
@@ -441,11 +568,54 @@ export async function createResourceLoader(options: CreateResourceLoaderOptions
|
|
|
441
568
|
return loader
|
|
442
569
|
}
|
|
443
570
|
|
|
444
|
-
function withOrigin(
|
|
571
|
+
function withOrigin(
|
|
572
|
+
systemPrompt: string,
|
|
573
|
+
origin: SessionOrigin | undefined,
|
|
574
|
+
permissions: PermissionService | undefined,
|
|
575
|
+
): string {
|
|
445
576
|
if (!origin) return systemPrompt
|
|
446
|
-
|
|
577
|
+
const roleContext = resolveRoleContext(origin, permissions)
|
|
578
|
+
return `${systemPrompt}\n\n${renderSessionOrigin(origin, Date.now(), roleContext)}`
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function resolveRoleContext(
|
|
582
|
+
origin: SessionOrigin,
|
|
583
|
+
permissions: PermissionService | undefined,
|
|
584
|
+
): SessionRoleContext | undefined {
|
|
585
|
+
if (permissions === undefined) return undefined
|
|
586
|
+
const described = permissions.describe(origin)
|
|
587
|
+
// TUI normally resolves to `owner` via the built-in `owner.match = [tui]`
|
|
588
|
+
// entry, and we skip the role block in that case to save tokens on every
|
|
589
|
+
// interactive session. But user-declared roles can match TUI first (the
|
|
590
|
+
// resolver is first-match-wins in declaration order), so a non-owner TUI
|
|
591
|
+
// role is possible and the agent needs to see it. The "TUI is always owner"
|
|
592
|
+
// shorthand in docs is the common case, not an invariant.
|
|
593
|
+
if (origin.kind === 'tui' && described.role === 'owner') return undefined
|
|
594
|
+
return described
|
|
447
595
|
}
|
|
448
596
|
|
|
449
597
|
export function getBundledSkillsDir(): string {
|
|
450
598
|
return join(dirname(fileURLToPath(import.meta.url)), '..', 'skills')
|
|
451
599
|
}
|
|
600
|
+
|
|
601
|
+
// Profile-fallback warning is fired once per (profile, ref) pair per process.
|
|
602
|
+
// Without rate-limiting, every memory-logger spawn (~every idle event) would
|
|
603
|
+
// emit a fresh warning when the user has only `default` configured — tens of
|
|
604
|
+
// warnings per channel session is noise the operator will learn to ignore.
|
|
605
|
+
// The pair includes `ref` so a config reload that changes `default` re-warns.
|
|
606
|
+
const profileFallbackWarned = new Set<string>()
|
|
607
|
+
|
|
608
|
+
function warnProfileFallbackOnce(profile: string, ref: string): void {
|
|
609
|
+
const key = `${profile}\x00${ref}`
|
|
610
|
+
if (profileFallbackWarned.has(key)) return
|
|
611
|
+
profileFallbackWarned.add(key)
|
|
612
|
+
console.warn(
|
|
613
|
+
`[agent] unknown model profile "${profile}"; falling back to "default" (${ref}). Add it under \`models\` in typeclaw.json to remove this warning. (further occurrences suppressed)`,
|
|
614
|
+
)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Test-only: clear the rate-limit cache so a test can assert the warning fires
|
|
618
|
+
// once after rate-limit reset.
|
|
619
|
+
export function __resetProfileFallbackWarningsForTesting(): void {
|
|
620
|
+
profileFallbackWarned.clear()
|
|
621
|
+
}
|