bazilion 0.0.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/.understand-anything/.understandignore +25 -0
- package/.understand-anything/fingerprints.json +14267 -0
- package/.understand-anything/knowledge-graph.json +18128 -0
- package/.understand-anything/meta.json +6 -0
- package/CLAUDE.md +164 -0
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/apps/cli/package.json +21 -0
- package/apps/cli/src/auth-file.ts +18 -0
- package/apps/cli/src/client.ts +37 -0
- package/apps/cli/src/columnize.ts +32 -0
- package/apps/cli/src/commands/agent.ts +574 -0
- package/apps/cli/src/commands/auth.ts +110 -0
- package/apps/cli/src/commands/backup.ts +135 -0
- package/apps/cli/src/commands/completion.ts +155 -0
- package/apps/cli/src/commands/config.ts +95 -0
- package/apps/cli/src/commands/doctor.ts +131 -0
- package/apps/cli/src/commands/group.ts +132 -0
- package/apps/cli/src/commands/inbox.ts +82 -0
- package/apps/cli/src/commands/login.ts +73 -0
- package/apps/cli/src/commands/memory.ts +106 -0
- package/apps/cli/src/commands/profile.ts +259 -0
- package/apps/cli/src/commands/provider.ts +170 -0
- package/apps/cli/src/commands/send.ts +24 -0
- package/apps/cli/src/commands/serve.ts +89 -0
- package/apps/cli/src/commands/skill.ts +120 -0
- package/apps/cli/src/commands/token.ts +148 -0
- package/apps/cli/src/commands/trigger.ts +129 -0
- package/apps/cli/src/commands/uninstall.ts +156 -0
- package/apps/cli/src/index.ts +196 -0
- package/apps/cli/src/paths.ts +12 -0
- package/apps/cli/test/agent.test.ts +213 -0
- package/apps/cli/test/backup.test.ts +95 -0
- package/apps/cli/test/chat.test.ts +539 -0
- package/apps/cli/test/columnize.test.ts +29 -0
- package/apps/cli/test/completion.test.ts +45 -0
- package/apps/cli/test/config-page.test.ts +151 -0
- package/apps/cli/test/group.test.ts +74 -0
- package/apps/cli/test/helpers.ts +70 -0
- package/apps/cli/test/inbox-autodeliver.test.ts +143 -0
- package/apps/cli/test/inbox.test.ts +147 -0
- package/apps/cli/test/memory.test.ts +69 -0
- package/apps/cli/test/profile.test.ts +110 -0
- package/apps/cli/test/send.test.ts +30 -0
- package/apps/cli/test/server-fixture.ts +212 -0
- package/apps/cli/test/session-head.test.ts +45 -0
- package/apps/cli/test/skill.test.ts +128 -0
- package/apps/cli/test/token.test.ts +109 -0
- package/apps/cli/test/trigger.test.ts +245 -0
- package/apps/cli/tsconfig.json +4 -0
- package/apps/daemon/package.json +31 -0
- package/apps/daemon/src/app.ts +41 -0
- package/apps/daemon/src/core/agent/archive.ts +8 -0
- package/apps/daemon/src/core/agent/delete.ts +30 -0
- package/apps/daemon/src/core/agent/resolve.ts +31 -0
- package/apps/daemon/src/core/agent/spawn.ts +95 -0
- package/apps/daemon/src/core/agent/unarchive.ts +11 -0
- package/apps/daemon/src/core/availableModels.ts +58 -0
- package/apps/daemon/src/core/db/client.ts +113 -0
- package/apps/daemon/src/core/db/migrate.ts +41 -0
- package/apps/daemon/src/core/db/migrations/0001_init.sql +179 -0
- package/apps/daemon/src/core/group/delete.ts +21 -0
- package/apps/daemon/src/core/group/register.ts +68 -0
- package/apps/daemon/src/core/index.ts +71 -0
- package/apps/daemon/src/core/paths.ts +56 -0
- package/apps/daemon/src/core/profile/create.ts +78 -0
- package/apps/daemon/src/core/profile/delete.ts +26 -0
- package/apps/daemon/src/core/profile/identity.ts +70 -0
- package/apps/daemon/src/core/profile/load.ts +45 -0
- package/apps/daemon/src/core/profile/seed.ts +74 -0
- package/apps/daemon/src/core/profile/templates.ts +60 -0
- package/apps/daemon/src/core/profile/update.ts +57 -0
- package/apps/daemon/src/core/profile/validate.ts +9 -0
- package/apps/daemon/src/core/repos/agents.ts +202 -0
- package/apps/daemon/src/core/repos/config.ts +78 -0
- package/apps/daemon/src/core/repos/groups.ts +55 -0
- package/apps/daemon/src/core/repos/messages.ts +127 -0
- package/apps/daemon/src/core/repos/profiles.ts +83 -0
- package/apps/daemon/src/core/repos/providerModels.ts +58 -0
- package/apps/daemon/src/core/repos/providerState.ts +37 -0
- package/apps/daemon/src/core/repos/secrets.ts +145 -0
- package/apps/daemon/src/core/repos/skillMeta.ts +49 -0
- package/apps/daemon/src/core/repos/triggers.ts +101 -0
- package/apps/daemon/src/core/repos/webTokens.ts +87 -0
- package/apps/daemon/src/core/secrets.ts +65 -0
- package/apps/daemon/src/core/services.ts +264 -0
- package/apps/daemon/src/core/skills/discover.ts +28 -0
- package/apps/daemon/src/core/skills/import.ts +136 -0
- package/apps/daemon/src/core/skills/parse.ts +52 -0
- package/apps/daemon/src/core/skills/resolve.ts +50 -0
- package/apps/daemon/src/index.ts +45 -0
- package/apps/daemon/src/lib/agent-cancel.ts +48 -0
- package/apps/daemon/src/lib/agent-id.ts +13 -0
- package/apps/daemon/src/lib/agent-turn.ts +56 -0
- package/apps/daemon/src/lib/api-key.ts +56 -0
- package/apps/daemon/src/lib/auth.ts +41 -0
- package/apps/daemon/src/lib/cron.ts +93 -0
- package/apps/daemon/src/lib/ctx.ts +80 -0
- package/apps/daemon/src/lib/messaging-host.ts +34 -0
- package/apps/daemon/src/lib/middleware-auth.ts +52 -0
- package/apps/daemon/src/lib/scheduler.ts +294 -0
- package/apps/daemon/src/routes/agents.ts +772 -0
- package/apps/daemon/src/routes/auth-login.ts +193 -0
- package/apps/daemon/src/routes/config.ts +267 -0
- package/apps/daemon/src/routes/groups.ts +133 -0
- package/apps/daemon/src/routes/messages.ts +29 -0
- package/apps/daemon/src/routes/misc.ts +239 -0
- package/apps/daemon/src/routes/profiles.ts +197 -0
- package/apps/daemon/src/routes/skills.ts +123 -0
- package/apps/daemon/src/routes/triggers.ts +29 -0
- package/apps/daemon/src/runtime/auth/openai-codex.ts +121 -0
- package/apps/daemon/src/runtime/auto-reply/heartbeat.ts +31 -0
- package/apps/daemon/src/runtime/index.ts +77 -0
- package/apps/daemon/src/runtime/memory/files.ts +103 -0
- package/apps/daemon/src/runtime/memory/qmd.ts +152 -0
- package/apps/daemon/src/runtime/memory/types.ts +16 -0
- package/apps/daemon/src/runtime/pi/events.ts +173 -0
- package/apps/daemon/src/runtime/pi/session.ts +536 -0
- package/apps/daemon/src/runtime/pi/tools.ts +85 -0
- package/apps/daemon/src/runtime/providers/catalog.ts +145 -0
- package/apps/daemon/src/runtime/providers/pi-adapter.ts +272 -0
- package/apps/daemon/src/runtime/providers/registry.ts +374 -0
- package/apps/daemon/src/runtime/providers/retry.ts +176 -0
- package/apps/daemon/src/runtime/providers/types.ts +33 -0
- package/apps/daemon/src/runtime/session/prompt.ts +83 -0
- package/apps/daemon/src/runtime/tools/bootstrap.ts +22 -0
- package/apps/daemon/src/runtime/tools/home.ts +114 -0
- package/apps/daemon/src/runtime/tools/memory.ts +81 -0
- package/apps/daemon/src/runtime/tools/messaging.ts +127 -0
- package/apps/daemon/src/runtime/tools/registry.ts +29 -0
- package/apps/daemon/src/runtime/tools/types.ts +13 -0
- package/apps/daemon/src/runtime/tools/web-extract.ts +110 -0
- package/apps/daemon/src/runtime/tools/web-ssrf.ts +245 -0
- package/apps/daemon/src/runtime/tools/web.ts +273 -0
- package/apps/daemon/src/runtime/worker/entry.ts +221 -0
- package/apps/daemon/src/runtime/worker/ipc-protocol.ts +75 -0
- package/apps/daemon/src/runtime/worker/spawn.ts +249 -0
- package/apps/daemon/test/core/agents.test.ts +319 -0
- package/apps/daemon/test/core/available-models.test.ts +63 -0
- package/apps/daemon/test/core/config.test.ts +99 -0
- package/apps/daemon/test/core/groups.test.ts +63 -0
- package/apps/daemon/test/core/helpers.ts +50 -0
- package/apps/daemon/test/core/identity.test.ts +85 -0
- package/apps/daemon/test/core/migrations.test.ts +83 -0
- package/apps/daemon/test/core/profiles.test.ts +182 -0
- package/apps/daemon/test/core/provider-models.test.ts +57 -0
- package/apps/daemon/test/core/provider-state.test.ts +36 -0
- package/apps/daemon/test/core/skill-meta.test.ts +45 -0
- package/apps/daemon/test/core/skills.test.ts +271 -0
- package/apps/daemon/test/core/triggers.test.ts +182 -0
- package/apps/daemon/test/core/web-tokens.test.ts +79 -0
- package/apps/daemon/test/cron.test.ts +90 -0
- package/apps/daemon/test/runtime/heartbeat.test.ts +34 -0
- package/apps/daemon/test/runtime/memory-qmd.test.ts +97 -0
- package/apps/daemon/test/runtime/memory.test.ts +78 -0
- package/apps/daemon/test/runtime/messaging.test.ts +213 -0
- package/apps/daemon/test/runtime/mock-server.ts +65 -0
- package/apps/daemon/test/runtime/openai-codex-auth.test.ts +116 -0
- package/apps/daemon/test/runtime/providers.test.ts +306 -0
- package/apps/daemon/test/runtime/retry.test.ts +191 -0
- package/apps/daemon/test/runtime/session-head.test.ts +90 -0
- package/apps/daemon/test/runtime/tools-home.test.ts +102 -0
- package/apps/daemon/test/runtime/tools-web.test.ts +206 -0
- package/apps/daemon/tsconfig.json +4 -0
- package/apps/mobile/README.md +60 -0
- package/apps/mobile/app/_layout.tsx +58 -0
- package/apps/mobile/app/agents/[id]/chat.tsx +486 -0
- package/apps/mobile/app/agents/[id]/index.tsx +166 -0
- package/apps/mobile/app/agents/index.tsx +212 -0
- package/apps/mobile/app/index.tsx +21 -0
- package/apps/mobile/app/pair.tsx +226 -0
- package/apps/mobile/app/settings.tsx +419 -0
- package/apps/mobile/app.json +49 -0
- package/apps/mobile/assets/adaptive-icon.png +0 -0
- package/apps/mobile/assets/favicon.png +0 -0
- package/apps/mobile/assets/icon.png +0 -0
- package/apps/mobile/assets/splash-icon.png +0 -0
- package/apps/mobile/babel.config.js +6 -0
- package/apps/mobile/metro.config.js +28 -0
- package/apps/mobile/package.json +44 -0
- package/apps/mobile/src/auth.ts +66 -0
- package/apps/mobile/src/pair-url.ts +48 -0
- package/apps/mobile/src/theme-context.tsx +88 -0
- package/apps/mobile/src/theme.ts +135 -0
- package/apps/mobile/test/pair-url.test.ts +46 -0
- package/apps/mobile/tsconfig.json +23 -0
- package/apps/web/components.json +25 -0
- package/apps/web/package.json +39 -0
- package/apps/web/public/baziu.svg +8 -0
- package/apps/web/src/components/AgentTabs.tsx +45 -0
- package/apps/web/src/components/BaziuLogo.tsx +21 -0
- package/apps/web/src/components/ChatPane.tsx +1033 -0
- package/apps/web/src/components/ConfigTabs.tsx +29 -0
- package/apps/web/src/components/CopyButton.tsx +68 -0
- package/apps/web/src/components/CreateGroupDialog.tsx +127 -0
- package/apps/web/src/components/FieldRow.tsx +94 -0
- package/apps/web/src/components/Footer.tsx +10 -0
- package/apps/web/src/components/PawIcon.tsx +15 -0
- package/apps/web/src/components/Sidebar.tsx +287 -0
- package/apps/web/src/components/SpawnDialog.tsx +129 -0
- package/apps/web/src/components/ThemeToggle.tsx +75 -0
- package/apps/web/src/components/TopNav.tsx +34 -0
- package/apps/web/src/components/ui/button.tsx +67 -0
- package/apps/web/src/components/ui/card.tsx +103 -0
- package/apps/web/src/components/ui/checkbox.tsx +31 -0
- package/apps/web/src/components/ui/dialog.tsx +168 -0
- package/apps/web/src/components/ui/input.tsx +19 -0
- package/apps/web/src/components/ui/label.tsx +22 -0
- package/apps/web/src/components/ui/radio-group.tsx +44 -0
- package/apps/web/src/components/ui/select.tsx +192 -0
- package/apps/web/src/components/ui/separator.tsx +26 -0
- package/apps/web/src/components/ui/table.tsx +116 -0
- package/apps/web/src/components/ui/tabs.tsx +88 -0
- package/apps/web/src/components/ui/textarea.tsx +18 -0
- package/apps/web/src/lib/auth.ts +50 -0
- package/apps/web/src/lib/daemon-client.ts +34 -0
- package/apps/web/src/lib/md.ts +45 -0
- package/apps/web/src/lib/utils.ts +6 -0
- package/apps/web/src/lib/wire-constants.ts +27 -0
- package/apps/web/src/routeTree.gen.ts +408 -0
- package/apps/web/src/router.tsx +20 -0
- package/apps/web/src/routes/__root.tsx +123 -0
- package/apps/web/src/routes/agents/$id/inbox.tsx +207 -0
- package/apps/web/src/routes/agents/$id/index.tsx +527 -0
- package/apps/web/src/routes/agents/$id/triggers.tsx +239 -0
- package/apps/web/src/routes/agents/index.tsx +265 -0
- package/apps/web/src/routes/api/$.ts +88 -0
- package/apps/web/src/routes/config/index.tsx +315 -0
- package/apps/web/src/routes/config/services.tsx +49 -0
- package/apps/web/src/routes/config/tokens.tsx +192 -0
- package/apps/web/src/routes/groups/$id/index.tsx +153 -0
- package/apps/web/src/routes/groups/$id/memory.tsx +321 -0
- package/apps/web/src/routes/groups/index.tsx +191 -0
- package/apps/web/src/routes/index.tsx +133 -0
- package/apps/web/src/routes/login.tsx +63 -0
- package/apps/web/src/routes/profiles/$id.tsx +549 -0
- package/apps/web/src/routes/profiles/index.tsx +458 -0
- package/apps/web/src/routes/skills/index.tsx +297 -0
- package/apps/web/src/routes/welcome.tsx +61 -0
- package/apps/web/src/styles.css +449 -0
- package/apps/web/tsconfig.json +25 -0
- package/apps/web/vite.config.ts +25 -0
- package/biome.json +23 -0
- package/docs/agent-engine.md +219 -0
- package/docs/architecture.md +627 -0
- package/docs/backlog/README.md +42 -0
- package/docs/backlog/draft/BAZ-001-a2a-federation-spike.md +125 -0
- package/docs/openclaw-reference.md +210 -0
- package/package.json +38 -0
- package/packages/api-types/package.json +11 -0
- package/packages/api-types/src/entities.ts +146 -0
- package/packages/api-types/src/events.ts +55 -0
- package/packages/api-types/src/index.ts +488 -0
- package/packages/api-types/src/memory.ts +15 -0
- package/packages/client/package.json +13 -0
- package/packages/client/src/index.ts +117 -0
- package/pnpm-workspace.yaml +3 -0
- package/tsconfig.base.json +23 -0
- package/tsconfig.json +11 -0
- package/vitest.config.ts +22 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// In-process scheduler for agent triggers (heartbeats + cron) and inbox
|
|
2
|
+
// auto-delivery.
|
|
3
|
+
//
|
|
4
|
+
// Each tick does two jobs:
|
|
5
|
+
//
|
|
6
|
+
// 1. Trigger firing. Loads enabled `agent_triggers` rows and fires whichever
|
|
7
|
+
// are due via `runAgentTurn`. A trigger is "due" when:
|
|
8
|
+
// - interval: last_fired_at + intervalSec*1000 ≤ now (never-fired
|
|
9
|
+
// uses created_at as the baseline)
|
|
10
|
+
// - cron: current minute's wall-clock matches expression AND
|
|
11
|
+
// last_fired_at's minute < current minute
|
|
12
|
+
//
|
|
13
|
+
// 2. Inbox auto-delivery (always on). Scans `messages` for recipients
|
|
14
|
+
// with unread mail. For each idle recipient (not already running /
|
|
15
|
+
// firing), drains all their unread messages in one transaction and
|
|
16
|
+
// fires a turn whose prompt embeds the messages. Marking read happens
|
|
17
|
+
// *inside* the drain transaction so two concurrent ticks can't
|
|
18
|
+
// double-dispatch. To disable both triggers AND auto-delivery, set
|
|
19
|
+
// `BAZILION_SCHEDULER=off` — there is no separate inbox-only knob
|
|
20
|
+
// because free inter-agent messaging is a baseline Bazilion promise.
|
|
21
|
+
//
|
|
22
|
+
// Concurrency: each trigger gets an in-memory "firing" guard so a slow turn
|
|
23
|
+
// can't pile up overlapping runs for the same trigger. Auto-delivery shares
|
|
24
|
+
// the same mechanism keyed on `msg-wake:<agentId>`. The DB is still the
|
|
25
|
+
// source of truth for last_fired_at / read_at — we mark *before* kicking the
|
|
26
|
+
// run, so a server restart won't immediately re-fire.
|
|
27
|
+
|
|
28
|
+
import type { AgentTrigger, Message } from '@bazilion/api-types'
|
|
29
|
+
import { agentRepo, messageRepo, triggerRepo } from '../core/index.ts'
|
|
30
|
+
import { isActiveAgent } from './agent-cancel.ts'
|
|
31
|
+
import { runAgentTurn } from './agent-turn.ts'
|
|
32
|
+
import { matchesCron, type ParsedCron, parseCron } from './cron.ts'
|
|
33
|
+
import { getCtx } from './ctx.ts'
|
|
34
|
+
|
|
35
|
+
const SCHEDULER_KEY = Symbol.for('bazilion.scheduler')
|
|
36
|
+
const TICK_MS = Number(process.env.BAZILION_SCHEDULER_TICK_MS ?? 5_000)
|
|
37
|
+
|
|
38
|
+
interface SchedulerState {
|
|
39
|
+
timer: NodeJS.Timeout | null
|
|
40
|
+
firing: Set<string>
|
|
41
|
+
cronCache: Map<string, ParsedCron>
|
|
42
|
+
/** pinned onStop for graceful shutdown (tests) */
|
|
43
|
+
stopped: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function state(): SchedulerState {
|
|
47
|
+
const g = globalThis as unknown as Record<symbol, SchedulerState | undefined>
|
|
48
|
+
let s = g[SCHEDULER_KEY]
|
|
49
|
+
if (!s) {
|
|
50
|
+
s = { timer: null, firing: new Set(), cronCache: new Map(), stopped: false }
|
|
51
|
+
g[SCHEDULER_KEY] = s
|
|
52
|
+
}
|
|
53
|
+
return s
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function floorToMinute(ms: number): number {
|
|
57
|
+
return Math.floor(ms / 60_000) * 60_000
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isDue(t: AgentTrigger, now: number, cronCache: Map<string, ParsedCron>): boolean {
|
|
61
|
+
if (!t.enabled) return false
|
|
62
|
+
if (t.kind === 'interval') {
|
|
63
|
+
const every = (t.intervalSec ?? 0) * 1000
|
|
64
|
+
if (every <= 0) return false
|
|
65
|
+
const baseline = t.lastFiredAt ?? t.createdAt
|
|
66
|
+
return now - baseline >= every
|
|
67
|
+
}
|
|
68
|
+
if (t.kind === 'cron') {
|
|
69
|
+
if (!t.cronExpr) return false
|
|
70
|
+
let parsed = cronCache.get(t.cronExpr)
|
|
71
|
+
if (!parsed) {
|
|
72
|
+
try {
|
|
73
|
+
parsed = parseCron(t.cronExpr)
|
|
74
|
+
} catch {
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
cronCache.set(t.cronExpr, parsed)
|
|
78
|
+
}
|
|
79
|
+
const nowFloor = floorToMinute(now)
|
|
80
|
+
if (t.lastFiredAt && floorToMinute(t.lastFiredAt) === nowFloor) return false
|
|
81
|
+
return matchesCron(parsed, new Date(nowFloor))
|
|
82
|
+
}
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function fireTrigger(t: AgentTrigger): Promise<void> {
|
|
87
|
+
const s = state()
|
|
88
|
+
if (s.firing.has(t.id)) return
|
|
89
|
+
s.firing.add(t.id)
|
|
90
|
+
const ctx = getCtx()
|
|
91
|
+
// Mark fired first — if the agent turn fails, we still don't want to loop
|
|
92
|
+
// on the same trigger every tick. The user will see it in `trigger list`
|
|
93
|
+
// (last_fired_at updated) and the run will be marked failed.
|
|
94
|
+
try {
|
|
95
|
+
triggerRepo.markFired(ctx.db, t.id)
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error(`[scheduler] markFired failed for ${t.id}:`, err)
|
|
98
|
+
s.firing.delete(t.id)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
// Drain the turn; we don't stream to anyone. Errors surface as `fatal`
|
|
103
|
+
// frames which we log but don't throw — the run row in the DB carries
|
|
104
|
+
// the real status.
|
|
105
|
+
for await (const frame of runAgentTurn(t.agentId, t.message)) {
|
|
106
|
+
if (frame.kind === 'fatal') {
|
|
107
|
+
console.error(`[scheduler] trigger ${t.id} fatal:`, frame.error)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error(`[scheduler] trigger ${t.id} unexpected throw:`, err)
|
|
112
|
+
} finally {
|
|
113
|
+
s.firing.delete(t.id)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function decodeText(payload: string): string {
|
|
118
|
+
try {
|
|
119
|
+
const obj = JSON.parse(payload) as { text?: unknown }
|
|
120
|
+
if (typeof obj.text === 'string') return obj.text
|
|
121
|
+
} catch {}
|
|
122
|
+
return payload
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Sentinel prepended to every inbox-wake prompt. The web chat UI detects it
|
|
126
|
+
// to render the bubble with a distinct "inter-agent" style instead of the
|
|
127
|
+
// default user-message styling. Kept in sync by copy with `chat.ts`'s
|
|
128
|
+
// INBOX_WAKE_PREFIX constant.
|
|
129
|
+
const INBOX_WAKE_PREFIX = '[[bazilion:inbox-wake]]\n'
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Build a wake-up prompt for an agent that has unread mail. The prompt is
|
|
133
|
+
* framed as a user-side system notice — not an agent-style message — so the
|
|
134
|
+
* recipient's LLM understands this is the runtime telling it "here's what
|
|
135
|
+
* arrived for you".
|
|
136
|
+
*
|
|
137
|
+
* Loop prevention: we differentiate by `replyTo`. A NEW message (no
|
|
138
|
+
* `replyTo`) opens a thread, and the sender is waiting for an answer — the
|
|
139
|
+
* agent must respond at least once. A REPLY (`replyTo` set) is already
|
|
140
|
+
* closing a round-trip, so the agent may acknowledge silently. This single
|
|
141
|
+
* asymmetry is enough to terminate conversations after one exchange instead
|
|
142
|
+
* of ping-ponging forever ("you have to answer me" → "no you have to
|
|
143
|
+
* answer me" → …).
|
|
144
|
+
*/
|
|
145
|
+
function buildInboxPrompt(
|
|
146
|
+
agentId: string,
|
|
147
|
+
messages: Message[],
|
|
148
|
+
fromNames: Map<string, string>,
|
|
149
|
+
): string {
|
|
150
|
+
const lines: string[] = [INBOX_WAKE_PREFIX.trimEnd()]
|
|
151
|
+
lines.push(
|
|
152
|
+
`You have ${messages.length} new message${messages.length === 1 ? '' : 's'} in your inbox:`,
|
|
153
|
+
)
|
|
154
|
+
lines.push('')
|
|
155
|
+
const newThread: Message[] = []
|
|
156
|
+
for (const m of messages) {
|
|
157
|
+
const name = fromNames.get(m.fromAgentId)
|
|
158
|
+
const fromLabel = name ? `${name} (${m.fromAgentId})` : m.fromAgentId
|
|
159
|
+
const text = decodeText(m.payload)
|
|
160
|
+
const header = m.replyTo
|
|
161
|
+
? `--- from ${fromLabel} (message ${m.id}, reply to ${m.replyTo}) ---`
|
|
162
|
+
: `--- from ${fromLabel} (message ${m.id}) ---`
|
|
163
|
+
lines.push(header)
|
|
164
|
+
lines.push(text)
|
|
165
|
+
lines.push('')
|
|
166
|
+
if (!m.replyTo) newThread.push(m)
|
|
167
|
+
}
|
|
168
|
+
if (newThread.length > 0) {
|
|
169
|
+
lines.push(
|
|
170
|
+
`You MUST reply to the ${newThread.length} new message${newThread.length === 1 ? '' : 's'} above ` +
|
|
171
|
+
'(the ones that are NOT marked as a reply). For each, call ' +
|
|
172
|
+
'`send_message(to=<sender agent id>, text=<your reply>, reply_to=<the message id shown in the header>)`. ' +
|
|
173
|
+
'Give a concrete answer if you can; a brief acknowledgement is fine if the message is purely informational. ' +
|
|
174
|
+
'Avoid asking follow-up questions — answer with what you already know. ' +
|
|
175
|
+
'IMPORTANT: do NOT demand a response from the sender in your reply — they already got what they asked for, ' +
|
|
176
|
+
'and asking them back to reply just creates infinite loops.',
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
const replies = messages.filter((m) => m.replyTo)
|
|
180
|
+
if (replies.length > 0) {
|
|
181
|
+
lines.push(
|
|
182
|
+
`The other message${replies.length === 1 ? ' is a reply' : 's are replies'} to something you previously sent. ` +
|
|
183
|
+
'Read, absorb, and move on — no response is required. Only send a follow-up if there is a ' +
|
|
184
|
+
'genuinely new question or action that requires it; otherwise this thread is closed.',
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
// reference the recipient id so the agent knows which mailbox this is for
|
|
188
|
+
// in case it was spawned without persona context
|
|
189
|
+
lines.push(`(recipient: ${agentId})`)
|
|
190
|
+
return lines.join('\n')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function fireInboxWake(agentId: string): Promise<void> {
|
|
194
|
+
const s = state()
|
|
195
|
+
const key = `msg-wake:${agentId}`
|
|
196
|
+
if (s.firing.has(key)) return
|
|
197
|
+
const ctx = getCtx()
|
|
198
|
+
// Skip agents with an active turn — they'll pick up the messages on their
|
|
199
|
+
// next natural turn or via the next tick after the current one ends.
|
|
200
|
+
if (isActiveAgent(agentId)) return
|
|
201
|
+
|
|
202
|
+
s.firing.add(key)
|
|
203
|
+
try {
|
|
204
|
+
const msgs = messageRepo.drainUnreadForAgent(ctx.db, agentId)
|
|
205
|
+
if (msgs.length === 0) {
|
|
206
|
+
// Raced with another consumer; nothing to do.
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
const fromIds = new Set(msgs.map((m) => m.fromAgentId))
|
|
210
|
+
const fromNames = new Map<string, string>()
|
|
211
|
+
for (const fid of fromIds) {
|
|
212
|
+
const sender = agentRepo.get(ctx.db, fid)
|
|
213
|
+
if (sender) fromNames.set(fid, sender.name)
|
|
214
|
+
}
|
|
215
|
+
const prompt = buildInboxPrompt(agentId, msgs, fromNames)
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
for await (const frame of runAgentTurn(agentId, prompt)) {
|
|
219
|
+
if (frame.kind === 'fatal') {
|
|
220
|
+
console.error(`[scheduler] inbox wake ${agentId} fatal:`, frame.error)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} catch (err) {
|
|
224
|
+
console.error(`[scheduler] inbox wake ${agentId} unexpected throw:`, err)
|
|
225
|
+
}
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.error(`[scheduler] inbox drain failed for ${agentId}:`, err)
|
|
228
|
+
} finally {
|
|
229
|
+
s.firing.delete(key)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function tick(): Promise<void> {
|
|
234
|
+
const s = state()
|
|
235
|
+
if (s.stopped) return
|
|
236
|
+
const now = Date.now()
|
|
237
|
+
let triggers: AgentTrigger[]
|
|
238
|
+
let recipients: string[] = []
|
|
239
|
+
try {
|
|
240
|
+
const ctx = getCtx()
|
|
241
|
+
triggers = triggerRepo.listEnabled(ctx.db)
|
|
242
|
+
recipients = messageRepo.listRecipientsWithUnread(ctx.db)
|
|
243
|
+
} catch (err) {
|
|
244
|
+
console.error('[scheduler] tick read failed:', err)
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
for (const t of triggers) {
|
|
248
|
+
if (isDue(t, now, s.cronCache)) {
|
|
249
|
+
// Fire async, don't await — the tick itself must stay fast.
|
|
250
|
+
void fireTrigger(t)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
for (const agentId of recipients) {
|
|
254
|
+
// Same fire-and-forget shape as triggers; `fireInboxWake` internally
|
|
255
|
+
// dedup-gates and bails if the agent already has an active run.
|
|
256
|
+
void fireInboxWake(agentId)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function startScheduler(): void {
|
|
261
|
+
const s = state()
|
|
262
|
+
// Replace any existing timer unconditionally so a duplicate startScheduler
|
|
263
|
+
// call (e.g. test harness calling getCtx twice) doesn't leave stale loops.
|
|
264
|
+
if (s.timer) clearInterval(s.timer)
|
|
265
|
+
s.stopped = false
|
|
266
|
+
s.timer = setInterval(() => {
|
|
267
|
+
void tick()
|
|
268
|
+
}, TICK_MS)
|
|
269
|
+
// Unref so the scheduler never blocks process exit in tests.
|
|
270
|
+
if (typeof s.timer.unref === 'function') s.timer.unref()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function stopScheduler(): void {
|
|
274
|
+
const s = state()
|
|
275
|
+
s.stopped = true
|
|
276
|
+
if (s.timer) {
|
|
277
|
+
clearInterval(s.timer)
|
|
278
|
+
s.timer = null
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Exposed for tests that want to drive the loop manually rather than wait
|
|
283
|
+
// on wall-clock intervals.
|
|
284
|
+
export async function _tickOnce(): Promise<void> {
|
|
285
|
+
await tick()
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function _isDueForTest(
|
|
289
|
+
t: AgentTrigger,
|
|
290
|
+
now: number,
|
|
291
|
+
cronCache: Map<string, ParsedCron> = new Map(),
|
|
292
|
+
): boolean {
|
|
293
|
+
return isDue(t, now, cronCache)
|
|
294
|
+
}
|