typeclaw 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -15
- package/auth.schema.json +113 -0
- package/package.json +1 -1
- package/secrets.schema.json +113 -0
- package/src/agent/auth.ts +4 -2
- package/src/agent/index.ts +16 -28
- package/src/agent/model-fallback.ts +127 -0
- package/src/agent/session-meta.ts +1 -1
- package/src/agent/session-origin.ts +3 -2
- package/src/agent/tools/curl-impersonate.ts +300 -0
- package/src/agent/tools/ddg.ts +13 -88
- package/src/agent/tools/webfetch/fetch.ts +105 -2
- package/src/agent/tools/webfetch/tool.ts +4 -0
- package/src/bundled-plugins/agent-browser/shim.ts +47 -0
- package/src/bundled-plugins/backup/subagents.ts +2 -0
- package/src/bundled-plugins/memory/README.md +49 -12
- package/src/bundled-plugins/memory/citation-superset.ts +63 -0
- package/src/bundled-plugins/memory/dreaming.ts +105 -17
- package/src/bundled-plugins/memory/index.ts +2 -2
- package/src/bundled-plugins/memory/memory-logger.ts +45 -26
- package/src/bundled-plugins/memory/strength.ts +127 -0
- package/src/bundled-plugins/memory/topics.ts +75 -0
- package/src/bundled-plugins/security/index.ts +88 -43
- package/src/bundled-plugins/security/permissions.ts +36 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +20 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +12 -0
- package/src/bundled-plugins/security/policies/prompt-injection.ts +23 -3
- package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +7 -0
- package/src/bundled-plugins/security/policies/secret-exfil-read.ts +6 -0
- package/src/bundled-plugins/security/policies/session-search-secrets.ts +9 -0
- package/src/bundled-plugins/security/policies/ssrf.ts +6 -0
- package/src/bundled-plugins/security/policies/system-prompt-leak.ts +7 -0
- package/src/channels/adapters/github/auth-app.ts +120 -0
- package/src/channels/adapters/github/auth-pat.ts +50 -0
- package/src/channels/adapters/github/auth.ts +33 -0
- package/src/channels/adapters/github/channel-resolver.ts +30 -0
- package/src/channels/adapters/github/dedup.ts +26 -0
- package/src/channels/adapters/github/event-allowlist.ts +8 -0
- package/src/channels/adapters/github/fetch-attachment.ts +5 -0
- package/src/channels/adapters/github/history.ts +63 -0
- package/src/channels/adapters/github/inbound.ts +286 -0
- package/src/channels/adapters/github/index.ts +370 -0
- package/src/channels/adapters/github/managed-path.ts +54 -0
- package/src/channels/adapters/github/membership.ts +35 -0
- package/src/channels/adapters/github/outbound.ts +145 -0
- package/src/channels/adapters/github/webhook-register.ts +349 -0
- package/src/channels/manager.ts +94 -9
- package/src/channels/router.ts +194 -28
- package/src/channels/schema.ts +31 -1
- package/src/channels/tunnel-bridge.ts +51 -0
- package/src/channels/types.ts +3 -1
- package/src/cli/builtins.ts +28 -0
- package/src/cli/channel.ts +511 -25
- package/src/cli/container-command-client.ts +244 -0
- package/src/cli/cron.ts +173 -0
- package/src/cli/host-command-runner.ts +150 -0
- package/src/cli/index.ts +42 -1
- package/src/cli/init.ts +400 -67
- package/src/cli/model.ts +14 -4
- package/src/cli/oauth-callbacks.ts +49 -0
- package/src/cli/plugin-command-help.ts +49 -0
- package/src/cli/plugin-commands-dispatch.ts +112 -0
- package/src/cli/plugin-commands.ts +118 -0
- package/src/cli/provider.ts +3 -20
- package/src/cli/tui.ts +10 -2
- package/src/cli/tunnel.ts +533 -0
- package/src/cli/ui.ts +8 -3
- package/src/config/config.ts +134 -24
- package/src/config/models-mutation.ts +42 -8
- package/src/config/providers-mutation.ts +12 -8
- package/src/container/start.ts +48 -4
- package/src/cron/bridge.ts +136 -0
- package/src/cron/consumer.ts +174 -48
- package/src/cron/index.ts +19 -2
- package/src/cron/list.ts +105 -0
- package/src/cron/scheduler.ts +12 -3
- package/src/cron/schema.ts +11 -3
- package/src/doctor/checks.ts +0 -50
- package/src/init/dockerfile.ts +165 -13
- package/src/init/ensure-deps.ts +15 -4
- package/src/init/github-webhook-install.ts +109 -0
- package/src/init/hatching.ts +2 -2
- package/src/init/index.ts +519 -12
- package/src/init/oauth-login.ts +17 -3
- package/src/init/run-bun-install.ts +17 -3
- package/src/init/run-owner-claim.ts +11 -2
- package/src/permissions/builtins.ts +29 -2
- package/src/permissions/match-rule.ts +24 -2
- package/src/permissions/permissions.ts +24 -7
- package/src/permissions/resolve.ts +1 -0
- package/src/plugin/define.ts +44 -1
- package/src/plugin/index.ts +18 -3
- package/src/plugin/manager.ts +16 -0
- package/src/plugin/registry.ts +85 -3
- package/src/plugin/types.ts +144 -1
- package/src/plugin/zod-introspect.ts +100 -0
- package/src/role-claim/match-rule.ts +2 -1
- package/src/run/index.ts +112 -4
- package/src/secrets/index.ts +1 -1
- package/src/secrets/schema.ts +21 -0
- package/src/server/command-runner.ts +476 -0
- package/src/server/index.ts +388 -5
- package/src/shared/index.ts +8 -0
- package/src/shared/protocol.ts +80 -1
- package/src/skills/typeclaw-channel-github/SKILL.md +24 -0
- package/src/skills/typeclaw-config/SKILL.md +27 -26
- package/src/skills/typeclaw-cron/SKILL.md +234 -3
- package/src/skills/typeclaw-memory/SKILL.md +25 -15
- package/src/skills/typeclaw-monorepo/SKILL.md +2 -2
- package/src/skills/typeclaw-permissions/SKILL.md +35 -16
- package/src/skills/typeclaw-plugins/SKILL.md +251 -5
- package/src/skills/typeclaw-tunnels/SKILL.md +111 -0
- package/src/test-helpers/wait-for.ts +50 -0
- package/src/tui/index.ts +70 -7
- package/src/tunnels/__fixtures__/cloudflared-quick-stderr.txt +11 -0
- package/src/tunnels/events.ts +14 -0
- package/src/tunnels/index.ts +12 -0
- package/src/tunnels/log-ring.ts +54 -0
- package/src/tunnels/manager.ts +139 -0
- package/src/tunnels/providers/cloudflare-quick.ts +189 -0
- package/src/tunnels/providers/external.ts +53 -0
- package/src/tunnels/quick-url-parser.ts +5 -0
- package/src/tunnels/types.ts +43 -0
- package/src/usage/report.ts +15 -12
- package/typeclaw.schema.json +311 -26
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ TypeClaw is the agent I wanted to use:
|
|
|
35
35
|
- 🔄 **Hot reload** — change `typeclaw.json`, `typeclaw reload` — no restart for most fields
|
|
36
36
|
- 🔁 **Self-restart** — the agent can bounce its own container when it updates itself
|
|
37
37
|
- 🌐 **Auto port-forward** — dev servers inside the container appear on `localhost`, even loopback-only ones
|
|
38
|
+
- 🌍 **Public tunnels** — Cloudflare Quick (zero signup) or bring-your-own external URL; the agent self-registers GitHub webhooks at the resulting public URL
|
|
38
39
|
- 🎼 **Compose** — orchestrate multiple agents across multiple folders
|
|
39
40
|
|
|
40
41
|
### 🌱 Self-improving, in detail
|
|
@@ -68,20 +69,23 @@ That's it. The agent is now alive, listening on a websocket, ready to receive pr
|
|
|
68
69
|
|
|
69
70
|
## CLI
|
|
70
71
|
|
|
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
|
|
84
|
-
| `typeclaw channel
|
|
72
|
+
| Command | Purpose |
|
|
73
|
+
| ----------------------------------- | ----------------------------------------------------------------------------------- |
|
|
74
|
+
| `typeclaw init` | Scaffold a new agent folder |
|
|
75
|
+
| `typeclaw start` | Build and run the container |
|
|
76
|
+
| `typeclaw stop` | Stop the container |
|
|
77
|
+
| `typeclaw restart` | `stop` then `start` |
|
|
78
|
+
| `typeclaw status` | Show container + daemon registration state |
|
|
79
|
+
| `typeclaw logs` | Stream container stdout/stderr with local timestamps; `-f` to follow |
|
|
80
|
+
| `typeclaw tui` | Attach a terminal UI over the agent's websocket |
|
|
81
|
+
| `typeclaw shell` | Open a shell inside the running container |
|
|
82
|
+
| `typeclaw reload` | Push a live config reload to the running agent |
|
|
83
|
+
| `typeclaw compose` | Orchestrate multiple agents |
|
|
84
|
+
| `typeclaw cron list` | List every cron job registered in the running agent (user `cron.json` + plugins) |
|
|
85
|
+
| `typeclaw channel add <kind>` | Wire a new channel adapter (Slack, Discord, Telegram, KakaoTalk, GitHub) |
|
|
86
|
+
| `typeclaw channel set <kind>` | Rotate the credentials of an already-configured channel (bot/app tokens, PAT, etc.) |
|
|
87
|
+
| `typeclaw channel reauth kakaotalk` | Re-authenticate KakaoTalk after a stale-token 401 or to rotate the stored password |
|
|
88
|
+
| `typeclaw tunnel ...` | Add/list/status/remove public tunnels and inspect tunnel logs |
|
|
85
89
|
|
|
86
90
|
## Configuration
|
|
87
91
|
|
|
@@ -107,7 +111,8 @@ my-agent/
|
|
|
107
111
|
- `plugins` — list of plugin module specifiers
|
|
108
112
|
- `channels` — `slack-bot` / `discord-bot` config
|
|
109
113
|
- `portForward` — allow/deny list for auto port forwarding (default: `*`)
|
|
110
|
-
- `
|
|
114
|
+
- `tunnels` — declare public URLs for inbound webhooks and ad-hoc exposure (`cloudflare-quick` or `external`)
|
|
115
|
+
- `dockerfile` — toggles for `gh`, `python`, `tmux`, `ffmpeg`, `cjkFonts`, plus `append` lines
|
|
111
116
|
- `memory` — idle window and dreaming schedule for the memory plugin
|
|
112
117
|
|
|
113
118
|
`Dockerfile` and `.gitignore` are owned by TypeClaw and rewritten on every `start` — edit `src/init/dockerfile.ts` and re-run `start --build` to ship template changes.
|
package/auth.schema.json
CHANGED
|
@@ -142,6 +142,119 @@
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
},
|
|
145
|
+
"github": {
|
|
146
|
+
"type": "object",
|
|
147
|
+
"properties": {
|
|
148
|
+
"auth": {
|
|
149
|
+
"oneOf": [
|
|
150
|
+
{
|
|
151
|
+
"type": "object",
|
|
152
|
+
"properties": {
|
|
153
|
+
"type": {
|
|
154
|
+
"type": "string",
|
|
155
|
+
"const": "pat"
|
|
156
|
+
},
|
|
157
|
+
"token": {
|
|
158
|
+
"anyOf": [
|
|
159
|
+
{
|
|
160
|
+
"type": "string",
|
|
161
|
+
"minLength": 1
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"type": "object",
|
|
165
|
+
"properties": {
|
|
166
|
+
"value": {
|
|
167
|
+
"type": "string",
|
|
168
|
+
"minLength": 1
|
|
169
|
+
},
|
|
170
|
+
"env": {
|
|
171
|
+
"type": "string",
|
|
172
|
+
"minLength": 1
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
"required": [
|
|
180
|
+
"type",
|
|
181
|
+
"token"
|
|
182
|
+
]
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"type": "object",
|
|
186
|
+
"properties": {
|
|
187
|
+
"type": {
|
|
188
|
+
"type": "string",
|
|
189
|
+
"const": "app"
|
|
190
|
+
},
|
|
191
|
+
"appId": {
|
|
192
|
+
"type": "integer",
|
|
193
|
+
"exclusiveMinimum": 0,
|
|
194
|
+
"maximum": 9007199254740991
|
|
195
|
+
},
|
|
196
|
+
"privateKey": {
|
|
197
|
+
"anyOf": [
|
|
198
|
+
{
|
|
199
|
+
"type": "string",
|
|
200
|
+
"minLength": 1
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"type": "object",
|
|
204
|
+
"properties": {
|
|
205
|
+
"value": {
|
|
206
|
+
"type": "string",
|
|
207
|
+
"minLength": 1
|
|
208
|
+
},
|
|
209
|
+
"env": {
|
|
210
|
+
"type": "string",
|
|
211
|
+
"minLength": 1
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
},
|
|
217
|
+
"installationId": {
|
|
218
|
+
"type": "integer",
|
|
219
|
+
"exclusiveMinimum": 0,
|
|
220
|
+
"maximum": 9007199254740991
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
"required": [
|
|
224
|
+
"type",
|
|
225
|
+
"appId",
|
|
226
|
+
"privateKey"
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
},
|
|
231
|
+
"webhookSecret": {
|
|
232
|
+
"anyOf": [
|
|
233
|
+
{
|
|
234
|
+
"type": "string",
|
|
235
|
+
"minLength": 1
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"type": "object",
|
|
239
|
+
"properties": {
|
|
240
|
+
"value": {
|
|
241
|
+
"type": "string",
|
|
242
|
+
"minLength": 1
|
|
243
|
+
},
|
|
244
|
+
"env": {
|
|
245
|
+
"type": "string",
|
|
246
|
+
"minLength": 1
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
]
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
"required": [
|
|
254
|
+
"auth",
|
|
255
|
+
"webhookSecret"
|
|
256
|
+
]
|
|
257
|
+
},
|
|
145
258
|
"telegram-bot": {
|
|
146
259
|
"type": "object",
|
|
147
260
|
"properties": {
|
package/package.json
CHANGED
package/secrets.schema.json
CHANGED
|
@@ -142,6 +142,119 @@
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
},
|
|
145
|
+
"github": {
|
|
146
|
+
"type": "object",
|
|
147
|
+
"properties": {
|
|
148
|
+
"auth": {
|
|
149
|
+
"oneOf": [
|
|
150
|
+
{
|
|
151
|
+
"type": "object",
|
|
152
|
+
"properties": {
|
|
153
|
+
"type": {
|
|
154
|
+
"type": "string",
|
|
155
|
+
"const": "pat"
|
|
156
|
+
},
|
|
157
|
+
"token": {
|
|
158
|
+
"anyOf": [
|
|
159
|
+
{
|
|
160
|
+
"type": "string",
|
|
161
|
+
"minLength": 1
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"type": "object",
|
|
165
|
+
"properties": {
|
|
166
|
+
"value": {
|
|
167
|
+
"type": "string",
|
|
168
|
+
"minLength": 1
|
|
169
|
+
},
|
|
170
|
+
"env": {
|
|
171
|
+
"type": "string",
|
|
172
|
+
"minLength": 1
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
"required": [
|
|
180
|
+
"type",
|
|
181
|
+
"token"
|
|
182
|
+
]
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"type": "object",
|
|
186
|
+
"properties": {
|
|
187
|
+
"type": {
|
|
188
|
+
"type": "string",
|
|
189
|
+
"const": "app"
|
|
190
|
+
},
|
|
191
|
+
"appId": {
|
|
192
|
+
"type": "integer",
|
|
193
|
+
"exclusiveMinimum": 0,
|
|
194
|
+
"maximum": 9007199254740991
|
|
195
|
+
},
|
|
196
|
+
"privateKey": {
|
|
197
|
+
"anyOf": [
|
|
198
|
+
{
|
|
199
|
+
"type": "string",
|
|
200
|
+
"minLength": 1
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"type": "object",
|
|
204
|
+
"properties": {
|
|
205
|
+
"value": {
|
|
206
|
+
"type": "string",
|
|
207
|
+
"minLength": 1
|
|
208
|
+
},
|
|
209
|
+
"env": {
|
|
210
|
+
"type": "string",
|
|
211
|
+
"minLength": 1
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
},
|
|
217
|
+
"installationId": {
|
|
218
|
+
"type": "integer",
|
|
219
|
+
"exclusiveMinimum": 0,
|
|
220
|
+
"maximum": 9007199254740991
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
"required": [
|
|
224
|
+
"type",
|
|
225
|
+
"appId",
|
|
226
|
+
"privateKey"
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
},
|
|
231
|
+
"webhookSecret": {
|
|
232
|
+
"anyOf": [
|
|
233
|
+
{
|
|
234
|
+
"type": "string",
|
|
235
|
+
"minLength": 1
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"type": "object",
|
|
239
|
+
"properties": {
|
|
240
|
+
"value": {
|
|
241
|
+
"type": "string",
|
|
242
|
+
"minLength": 1
|
|
243
|
+
},
|
|
244
|
+
"env": {
|
|
245
|
+
"type": "string",
|
|
246
|
+
"minLength": 1
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
]
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
"required": [
|
|
254
|
+
"auth",
|
|
255
|
+
"webhookSecret"
|
|
256
|
+
]
|
|
257
|
+
},
|
|
145
258
|
"telegram-bot": {
|
|
146
259
|
"type": "object",
|
|
147
260
|
"properties": {
|
package/src/agent/auth.ts
CHANGED
|
@@ -83,8 +83,10 @@ export function getAuthFor(providerId: KnownProviderId): Auth {
|
|
|
83
83
|
|
|
84
84
|
// Back-compat shim for callers that still want the `default` profile's auth
|
|
85
85
|
// (the main session path). Equivalent to `getAuthFor(provider-of-default)`.
|
|
86
|
+
// Uses the head of the fallback chain; auth for the rest of the chain is
|
|
87
|
+
// resolved lazily when fallback actually fires.
|
|
86
88
|
export function getAuth(): Auth {
|
|
87
|
-
const defaultRef = getConfig().models.default
|
|
89
|
+
const defaultRef = getConfig().models.default[0]!
|
|
88
90
|
return getAuthFor(providerForModelRef(defaultRef))
|
|
89
91
|
}
|
|
90
92
|
|
|
@@ -98,7 +100,7 @@ function hasAnyCredentialInEnv(apiKeyEnv: string | null): boolean {
|
|
|
98
100
|
|
|
99
101
|
function missingCredentialMessage(providerId: KnownProviderId): string {
|
|
100
102
|
const provider = KNOWN_PROVIDERS[providerId]
|
|
101
|
-
const defaultRef = getConfig().models.default
|
|
103
|
+
const defaultRef = getConfig().models.default[0]!
|
|
102
104
|
const defaultProviderId = providerForModelRef(defaultRef)
|
|
103
105
|
// For the `default` profile, name the model in the error message (matches
|
|
104
106
|
// pre-multi-model behavior). For any other profile, the user is mixing
|
package/src/agent/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { AgentSession, ToolDefinition } from '@mariozechner/pi-coding-agent
|
|
|
8
8
|
import { loadMemory } from '@/bundled-plugins/memory/load-memory'
|
|
9
9
|
import type { ChannelRouter } from '@/channels/router'
|
|
10
10
|
import { getConfig, resolveModel, resolveProfile } from '@/config'
|
|
11
|
-
import { providerForModelRef } from '@/config/providers'
|
|
11
|
+
import { providerForModelRef, type KnownModelRef } from '@/config/providers'
|
|
12
12
|
import type { PermissionService } from '@/permissions'
|
|
13
13
|
import type {
|
|
14
14
|
BuiltinToolRef,
|
|
@@ -134,6 +134,12 @@ export type CreateSessionOptions = {
|
|
|
134
134
|
// overrides) so different sessions on the same agent can run different
|
|
135
135
|
// models without per-session config edits.
|
|
136
136
|
profile?: string
|
|
137
|
+
// Override the resolved ref directly, bypassing `profile` resolution. Used
|
|
138
|
+
// by the model-fallback helper (`promptWithFallback`) to recreate a session
|
|
139
|
+
// pinned to the next ref in the chain after the previous one failed. When
|
|
140
|
+
// set, `profile` is still recorded for the fallback-warning bookkeeping;
|
|
141
|
+
// the profile→refs resolution is skipped.
|
|
142
|
+
refOverride?: KnownModelRef
|
|
137
143
|
// Defensive ceiling on cumulative bytes of tool-result text per session,
|
|
138
144
|
// applied to the named tools only. See `src/agent/tool-result-budget.ts`
|
|
139
145
|
// for the rationale. Intended for subagents that read large files
|
|
@@ -161,10 +167,14 @@ export async function createSession(options: CreateSessionOptions = {}): Promise
|
|
|
161
167
|
|
|
162
168
|
export async function createSessionWithDispose(options: CreateSessionOptions = {}): Promise<CreateSessionResult> {
|
|
163
169
|
const resolved = resolveProfile(getConfig().models, options.profile)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
170
|
+
// Unknown profiles silently fall back to `default`. The fallback is by design
|
|
171
|
+
// (see `resolveProfile`) and surfacing a warning here just creates noise on
|
|
172
|
+
// every memory-logger / dreaming subagent spawn for advanced users who know
|
|
173
|
+
// exactly what they're doing.
|
|
174
|
+
// `refOverride` lets the model-fallback helper pin a specific entry from
|
|
175
|
+
// the chain when it recreates a session after the previous ref failed.
|
|
176
|
+
const activeRef: KnownModelRef = options.refOverride ?? resolved.ref
|
|
177
|
+
const { authStorage, modelRegistry } = getAuthFor(providerForModelRef(activeRef))
|
|
168
178
|
|
|
169
179
|
const materializedSkills =
|
|
170
180
|
options.plugins && options.plugins.registry.skills.length > 0
|
|
@@ -279,7 +289,7 @@ export async function createSessionWithDispose(options: CreateSessionOptions = {
|
|
|
279
289
|
? customToolsPreBudget.map((t) => wrapToolDefinitionWithBudget(t, sessionBudget, sessionBudgetState))
|
|
280
290
|
: customToolsPreBudget
|
|
281
291
|
|
|
282
|
-
const model = resolveModel(
|
|
292
|
+
const model = resolveModel(activeRef)
|
|
283
293
|
const { session } = await createAgentSession({
|
|
284
294
|
model,
|
|
285
295
|
sessionManager,
|
|
@@ -737,25 +747,3 @@ function resolveRoleContext(
|
|
|
737
747
|
export function getBundledSkillsDir(): string {
|
|
738
748
|
return join(dirname(fileURLToPath(import.meta.url)), '..', 'skills')
|
|
739
749
|
}
|
|
740
|
-
|
|
741
|
-
// Profile-fallback warning is fired once per (profile, ref) pair per process.
|
|
742
|
-
// Without rate-limiting, every memory-logger spawn (~every idle event) would
|
|
743
|
-
// emit a fresh warning when the user has only `default` configured — tens of
|
|
744
|
-
// warnings per channel session is noise the operator will learn to ignore.
|
|
745
|
-
// The pair includes `ref` so a config reload that changes `default` re-warns.
|
|
746
|
-
const profileFallbackWarned = new Set<string>()
|
|
747
|
-
|
|
748
|
-
function warnProfileFallbackOnce(profile: string, ref: string): void {
|
|
749
|
-
const key = `${profile}\x00${ref}`
|
|
750
|
-
if (profileFallbackWarned.has(key)) return
|
|
751
|
-
profileFallbackWarned.add(key)
|
|
752
|
-
console.warn(
|
|
753
|
-
`[agent] unknown model profile "${profile}"; falling back to "default" (${ref}). Add it under \`models\` in typeclaw.json to remove this warning. (further occurrences suppressed)`,
|
|
754
|
-
)
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Test-only: clear the rate-limit cache so a test can assert the warning fires
|
|
758
|
-
// once after rate-limit reset.
|
|
759
|
-
export function __resetProfileFallbackWarningsForTesting(): void {
|
|
760
|
-
profileFallbackWarned.clear()
|
|
761
|
-
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { resolveProfile } from '@/config'
|
|
2
|
+
import type { Models } from '@/config/config'
|
|
3
|
+
import type { KnownModelRef } from '@/config/providers'
|
|
4
|
+
|
|
5
|
+
import type { AgentSession } from './index'
|
|
6
|
+
import { subscribeProviderErrors } from './provider-error'
|
|
7
|
+
|
|
8
|
+
// Result of a single fallback-aware prompt run.
|
|
9
|
+
// - `refUsed` is the ref whose session ultimately handled the turn.
|
|
10
|
+
// - `attempts` lists every ref that was tried, in order, with the failure
|
|
11
|
+
// reason for each attempt that didn't make it through. `attempts.length`
|
|
12
|
+
// is always >= 1; the last entry succeeded iff `success: true`.
|
|
13
|
+
// - `session` / `dispose` are the session that handled the turn (or attempted
|
|
14
|
+
// the final entry, on full-chain failure). Callers that need to keep using
|
|
15
|
+
// the session for subsequent turns store these in their state; callers that
|
|
16
|
+
// tear down per-turn (cron) just call `dispose()` and discard.
|
|
17
|
+
export type FallbackPromptResult = {
|
|
18
|
+
success: boolean
|
|
19
|
+
refUsed: KnownModelRef
|
|
20
|
+
attempts: FallbackAttempt[]
|
|
21
|
+
session: AgentSession
|
|
22
|
+
dispose: () => Promise<void>
|
|
23
|
+
// When `success === false`, this is the error from the final attempt.
|
|
24
|
+
lastError?: Error
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type FallbackAttempt = {
|
|
28
|
+
ref: KnownModelRef
|
|
29
|
+
// 'hard' = session.prompt() threw. 'soft' = pi-coding-agent surfaced an
|
|
30
|
+
// upstream error via stopReason: 'error' on the final assistant message.
|
|
31
|
+
// 'success' = the turn finished cleanly.
|
|
32
|
+
outcome: 'hard' | 'soft' | 'success'
|
|
33
|
+
errorMessage?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Build the ordered list of refs to attempt for a given profile. Single-ref
|
|
37
|
+
// profiles produce a length-1 chain; the fallback path is then a no-op in
|
|
38
|
+
// practice (the first attempt either succeeds or the error propagates).
|
|
39
|
+
//
|
|
40
|
+
// Exported so callers can introspect the chain (e.g. logs, telemetry) before
|
|
41
|
+
// firing the prompt — useful for `[cron] ${jobId}: trying chain a → b → c`.
|
|
42
|
+
export function resolveFallbackChain(models: Models, profile: string | undefined): KnownModelRef[] {
|
|
43
|
+
return resolveProfile(models, profile).refs
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Drives one `session.prompt(text)` call with full fallback semantics:
|
|
47
|
+
//
|
|
48
|
+
// 1. Create a session bound to `refs[0]` via `createSessionForRef`.
|
|
49
|
+
// 2. Subscribe to provider-error events so soft errors (pi-coding-agent's
|
|
50
|
+
// `stopReason: 'error'` shape) trigger fallback in addition to throws.
|
|
51
|
+
// 3. Await `session.prompt(text)`.
|
|
52
|
+
// 4. If the prompt threw OR a soft error fired during the turn:
|
|
53
|
+
// - dispose the failed session
|
|
54
|
+
// - advance to `refs[i+1]` and retry (only if a fallback is available)
|
|
55
|
+
// 5. Return the session that handled the turn (or the last-tried session
|
|
56
|
+
// on full-chain failure), the ref used, and the attempt log.
|
|
57
|
+
//
|
|
58
|
+
// The wrapper intentionally does NOT swallow the final failure: when every
|
|
59
|
+
// ref in the chain has been exhausted, the returned `success: false` plus
|
|
60
|
+
// `lastError` lets the caller surface the failure however it already does
|
|
61
|
+
// (console.error in the server drain, channel reaction in the router,
|
|
62
|
+
// cron-job status). This keeps the helper composable with the existing
|
|
63
|
+
// error-handling code at each call site.
|
|
64
|
+
export async function promptWithFallback(opts: {
|
|
65
|
+
refs: KnownModelRef[]
|
|
66
|
+
text: string
|
|
67
|
+
createSessionForRef: (ref: KnownModelRef) => Promise<{ session: AgentSession; dispose: () => Promise<void> }>
|
|
68
|
+
// Called after each non-final attempt so callers can log the per-attempt
|
|
69
|
+
// failure with their own context (sessionId, channel key, job id, ...).
|
|
70
|
+
onAttemptFailed?: (attempt: FallbackAttempt) => void
|
|
71
|
+
}): Promise<FallbackPromptResult> {
|
|
72
|
+
if (opts.refs.length === 0) {
|
|
73
|
+
throw new Error('promptWithFallback: refs[] must be non-empty')
|
|
74
|
+
}
|
|
75
|
+
const attempts: FallbackAttempt[] = []
|
|
76
|
+
let lastError: Error | undefined
|
|
77
|
+
for (let i = 0; i < opts.refs.length; i++) {
|
|
78
|
+
const ref = opts.refs[i]!
|
|
79
|
+
const isLast = i === opts.refs.length - 1
|
|
80
|
+
const { session, dispose } = await opts.createSessionForRef(ref)
|
|
81
|
+
// Capture the first soft error per attempt. The `subscribeProviderErrors`
|
|
82
|
+
// listener fires synchronously off the `message_end` event, which lands
|
|
83
|
+
// BEFORE `session.prompt()` resolves — so by the time `await` returns,
|
|
84
|
+
// `softError` is populated if a soft error occurred.
|
|
85
|
+
let softError: Error | undefined
|
|
86
|
+
const unsub = subscribeProviderErrors(session, (err) => {
|
|
87
|
+
if (!softError) softError = new Error(err.message)
|
|
88
|
+
})
|
|
89
|
+
try {
|
|
90
|
+
try {
|
|
91
|
+
await session.prompt(opts.text)
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
94
|
+
const attempt: FallbackAttempt = { ref, outcome: 'hard', errorMessage: error.message }
|
|
95
|
+
attempts.push(attempt)
|
|
96
|
+
lastError = error
|
|
97
|
+
if (!isLast) opts.onAttemptFailed?.(attempt)
|
|
98
|
+
unsub()
|
|
99
|
+
await dispose()
|
|
100
|
+
if (isLast) {
|
|
101
|
+
return { success: false, refUsed: ref, attempts, session, dispose: async () => {}, lastError }
|
|
102
|
+
}
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
if (softError !== undefined) {
|
|
106
|
+
const attempt: FallbackAttempt = { ref, outcome: 'soft', errorMessage: softError.message }
|
|
107
|
+
attempts.push(attempt)
|
|
108
|
+
lastError = softError
|
|
109
|
+
if (!isLast) opts.onAttemptFailed?.(attempt)
|
|
110
|
+
unsub()
|
|
111
|
+
await dispose()
|
|
112
|
+
if (isLast) {
|
|
113
|
+
return { success: false, refUsed: ref, attempts, session, dispose: async () => {}, lastError }
|
|
114
|
+
}
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
attempts.push({ ref, outcome: 'success' })
|
|
118
|
+
unsub()
|
|
119
|
+
return { success: true, refUsed: ref, attempts, session, dispose }
|
|
120
|
+
} catch (err) {
|
|
121
|
+
unsub()
|
|
122
|
+
await dispose()
|
|
123
|
+
throw err
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
throw new Error('promptWithFallback: unreachable — loop terminated without returning')
|
|
127
|
+
}
|
|
@@ -8,7 +8,7 @@ export type SessionMetaPayload = {
|
|
|
8
8
|
|
|
9
9
|
export type MinimalSessionOrigin =
|
|
10
10
|
| { kind: 'tui' }
|
|
11
|
-
| { kind: 'cron'; jobId: string; jobKind: 'prompt' | 'exec' | 'subagent' }
|
|
11
|
+
| { kind: 'cron'; jobId: string; jobKind: 'prompt' | 'exec' | 'subagent' | 'handler' }
|
|
12
12
|
| { kind: 'channel'; adapter: string; workspace: string; chat: string; thread: string | null }
|
|
13
13
|
| { kind: 'subagent'; subagent: string; parentSessionId: string }
|
|
14
14
|
|
|
@@ -25,7 +25,7 @@ export type SessionOrigin =
|
|
|
25
25
|
| {
|
|
26
26
|
kind: 'cron'
|
|
27
27
|
jobId: string
|
|
28
|
-
jobKind: 'prompt' | 'exec' | 'subagent'
|
|
28
|
+
jobKind: 'prompt' | 'exec' | 'subagent' | 'handler'
|
|
29
29
|
scheduledByRole?: string
|
|
30
30
|
scheduledByOrigin?: SessionOrigin | { kind: 'config-file' }
|
|
31
31
|
}
|
|
@@ -78,6 +78,7 @@ type PlatformInfo = {
|
|
|
78
78
|
const PLATFORM_INFO: Record<AdapterId, PlatformInfo> = {
|
|
79
79
|
'slack-bot': { displayName: 'Slack', mentionMode: 'angle-id' },
|
|
80
80
|
'discord-bot': { displayName: 'Discord', mentionMode: 'angle-id' },
|
|
81
|
+
github: { displayName: 'GitHub', mentionMode: 'at-username' },
|
|
81
82
|
'telegram-bot': { displayName: 'Telegram', mentionMode: 'at-username' },
|
|
82
83
|
kakaotalk: { displayName: 'KakaoTalk', mentionMode: 'alias' },
|
|
83
84
|
}
|
|
@@ -150,7 +151,7 @@ function renderTuiOrigin(): string {
|
|
|
150
151
|
].join('\n')
|
|
151
152
|
}
|
|
152
153
|
|
|
153
|
-
function renderCronOrigin(origin: { jobId: string; jobKind: 'prompt' | 'exec' | 'subagent' }): string {
|
|
154
|
+
function renderCronOrigin(origin: { jobId: string; jobKind: 'prompt' | 'exec' | 'subagent' | 'handler' }): string {
|
|
154
155
|
return [
|
|
155
156
|
'## Session origin',
|
|
156
157
|
'',
|