@vellumai/assistant 0.6.0 → 0.6.1
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/AGENTS.md +4 -0
- package/ARCHITECTURE.md +68 -15
- package/Dockerfile +2 -2
- package/bun.lock +6 -2
- package/docker-entrypoint.sh +32 -1
- package/docs/architecture/integrations.md +1 -1
- package/docs/architecture/memory.md +21 -24
- package/openapi.yaml +538 -3
- package/package.json +5 -1
- package/src/__tests__/anthropic-provider.test.ts +160 -95
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +47 -1
- package/src/__tests__/app-source-watcher.test.ts +159 -0
- package/src/__tests__/checker.test.ts +38 -6
- package/src/__tests__/config-schema.test.ts +5 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
- package/src/__tests__/conversation-agent-loop.test.ts +4 -51
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
- package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
- package/src/__tests__/conversation-wipe.test.ts +2 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
- package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
- package/src/__tests__/date-context.test.ts +76 -210
- package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
- package/src/__tests__/file-list-tool.test.ts +219 -0
- package/src/__tests__/first-greeting.test.ts +1 -1
- package/src/__tests__/heartbeat-service.test.ts +180 -3
- package/src/__tests__/identity-routes.test.ts +328 -0
- package/src/__tests__/injection-block.test.ts +24 -0
- package/src/__tests__/install-skill-routing.test.ts +7 -6
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
- package/src/__tests__/llm-context-normalization.test.ts +18 -18
- package/src/__tests__/llm-context-route-provider.test.ts +101 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
- package/src/__tests__/log-export-workspace.test.ts +72 -105
- package/src/__tests__/mcp-abort-signal.test.ts +5 -0
- package/src/__tests__/mcp-client-auth.test.ts +5 -0
- package/src/__tests__/memory-recall-log-store.test.ts +132 -0
- package/src/__tests__/migration-export-streaming.test.ts +304 -0
- package/src/__tests__/migration-import-commit-http.test.ts +11 -10
- package/src/__tests__/mock-fetch.ts +87 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
- package/src/__tests__/onboarding-template-contract.test.ts +62 -14
- package/src/__tests__/parser.test.ts +32 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
- package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
- package/src/__tests__/permission-mode-sse.test.ts +418 -0
- package/src/__tests__/permission-mode-store.test.ts +277 -0
- package/src/__tests__/permission-mode.test.ts +101 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
- package/src/__tests__/profiler-routes.test.ts +502 -0
- package/src/__tests__/profiler-run-store.test.ts +441 -0
- package/src/__tests__/proxy-approval-callback.test.ts +4 -75
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/sandbox-host-parity.test.ts +5 -4
- package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
- package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
- package/src/__tests__/search-skills-unified.test.ts +4 -3
- package/src/__tests__/send-endpoint-busy.test.ts +42 -3
- package/src/__tests__/set-permission-mode.test.ts +274 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
- package/src/__tests__/skill-memory.test.ts +2 -783
- package/src/__tests__/strip-memory-injections.test.ts +187 -0
- package/src/__tests__/subagent-detail.test.ts +84 -0
- package/src/__tests__/subagent-disposal.test.ts +308 -0
- package/src/__tests__/subagent-manager-notify.test.ts +19 -10
- package/src/__tests__/subagent-notify-parent.test.ts +390 -0
- package/src/__tests__/subagent-role-registry.test.ts +108 -0
- package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
- package/src/__tests__/subagent-tools.test.ts +464 -4
- package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
- package/src/__tests__/task-memory-cleanup.test.ts +12 -12
- package/src/__tests__/terminal-tools.test.ts +17 -27
- package/src/__tests__/test-preload.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +4 -26
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
- package/src/__tests__/top-level-renderer.test.ts +10 -13
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
- package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
- package/src/agent/loop.ts +6 -0
- package/src/approvals/guardian-request-resolvers.ts +24 -0
- package/src/avatar/traits-png-sync.ts +3 -3
- package/src/cli/__tests__/run-assistant-command.ts +29 -0
- package/src/cli/commands/__tests__/email-download.test.ts +245 -0
- package/src/cli/commands/__tests__/email-list.test.ts +192 -0
- package/src/cli/commands/__tests__/email-register.test.ts +186 -0
- package/src/cli/commands/__tests__/email-send.test.ts +291 -0
- package/src/cli/commands/__tests__/email-status.test.ts +181 -0
- package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
- package/src/cli/commands/__tests__/routes.test.ts +562 -0
- package/src/cli/commands/conversations.ts +1 -8
- package/src/cli/commands/email.ts +584 -835
- package/src/cli/commands/memory.ts +1 -34
- package/src/cli/commands/notifications.ts +7 -2
- package/src/cli/commands/oauth/connect.ts +14 -5
- package/src/cli/commands/routes.ts +396 -0
- package/src/cli/commands/skills.ts +130 -20
- package/src/cli/program.ts +2 -0
- package/src/cli.ts +1 -120
- package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
- package/src/config/bundled-skills/gmail/SKILL.md +2 -2
- package/src/config/bundled-skills/messaging/SKILL.md +7 -0
- package/src/config/bundled-skills/schedule/SKILL.md +22 -2
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
- package/src/config/bundled-skills/slack/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/SKILL.md +43 -3
- package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
- package/src/config/env-registry.ts +63 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/filing.ts +51 -0
- package/src/config/schemas/heartbeat.ts +15 -12
- package/src/config/schemas/memory-lifecycle.ts +12 -0
- package/src/config/schemas/security.ts +14 -0
- package/src/daemon/app-source-watcher.ts +93 -0
- package/src/daemon/config-watcher.ts +79 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
- package/src/daemon/conversation-agent-loop.ts +158 -65
- package/src/daemon/conversation-history.ts +4 -19
- package/src/daemon/conversation-lifecycle.ts +8 -14
- package/src/daemon/conversation-process.ts +13 -7
- package/src/daemon/conversation-runtime-assembly.ts +300 -306
- package/src/daemon/conversation-tool-setup.ts +44 -14
- package/src/daemon/conversation-workspace.ts +1 -2
- package/src/daemon/conversation.ts +18 -0
- package/src/daemon/date-context.ts +26 -53
- package/src/daemon/first-greeting.ts +1 -1
- package/src/daemon/handlers/conversations.ts +4 -7
- package/src/daemon/handlers/shared.test.ts +143 -0
- package/src/daemon/handlers/shared.ts +63 -5
- package/src/daemon/handlers/skills.ts +11 -18
- package/src/daemon/lifecycle.ts +199 -157
- package/src/daemon/message-types/conversations.ts +25 -6
- package/src/daemon/message-types/messages.ts +9 -1
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/settings.ts +6 -0
- package/src/daemon/profiler-run-store.ts +557 -0
- package/src/daemon/server.ts +89 -9
- package/src/daemon/shutdown-handlers.ts +5 -0
- package/src/daemon/tool-side-effects.ts +23 -3
- package/src/export/transcript-formatter.ts +148 -0
- package/src/filing/filing-service.ts +228 -0
- package/src/heartbeat/heartbeat-service.ts +96 -7
- package/src/mcp/client.ts +6 -0
- package/src/mcp/mcp-oauth-provider.ts +149 -27
- package/src/memory/admin.ts +33 -32
- package/src/memory/app-store.ts +69 -0
- package/src/memory/conversation-bootstrap.ts +1 -1
- package/src/memory/conversation-crud.ts +136 -107
- package/src/memory/conversation-group-migration.ts +1 -1
- package/src/memory/conversation-queries.ts +58 -12
- package/src/memory/conversation-title-service.ts +1 -0
- package/src/memory/db-init.ts +182 -376
- package/src/memory/graph/bootstrap.ts +75 -66
- package/src/memory/graph/capability-seed.ts +167 -15
- package/src/memory/graph/consolidation.ts +38 -4
- package/src/memory/graph/conversation-graph-memory.ts +133 -104
- package/src/memory/graph/extraction-job.ts +9 -4
- package/src/memory/graph/extraction.ts +66 -23
- package/src/memory/graph/graph-memory-state-store.ts +37 -0
- package/src/memory/graph/graph-search.ts +29 -15
- package/src/memory/graph/injection.ts +38 -8
- package/src/memory/graph/inspect.ts +12 -3
- package/src/memory/graph/retriever.ts +365 -262
- package/src/memory/graph/store.test.ts +48 -0
- package/src/memory/graph/store.ts +150 -11
- package/src/memory/graph/tool-handlers.ts +84 -209
- package/src/memory/graph/tools.ts +8 -52
- package/src/memory/graph/types.ts +24 -0
- package/src/memory/job-handlers/cleanup.ts +44 -1
- package/src/memory/jobs-store.ts +70 -60
- package/src/memory/jobs-worker.ts +44 -28
- package/src/memory/llm-request-log-store.ts +96 -12
- package/src/memory/memory-recall-log-store.ts +49 -5
- package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
- package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
- package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
- package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
- package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
- package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
- package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
- package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
- package/src/memory/migrations/index.ts +8 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +14 -0
- package/src/memory/schema/infrastructure.ts +8 -1
- package/src/memory/schema/memory-core.ts +0 -51
- package/src/memory/schema/memory-graph.ts +15 -0
- package/src/memory/task-memory-cleanup.ts +30 -11
- package/src/notifications/copy-composer.ts +86 -0
- package/src/notifications/decision-engine.ts +35 -0
- package/src/permissions/checker.ts +12 -1
- package/src/permissions/permission-mode-store.ts +180 -0
- package/src/permissions/permission-mode.ts +31 -0
- package/src/permissions/workspace-policy.ts +9 -0
- package/src/prompts/system-prompt.ts +59 -7
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
- package/src/prompts/templates/BOOTSTRAP.md +70 -165
- package/src/prompts/templates/HEARTBEAT.md +3 -1
- package/src/prompts/templates/SOUL.md +25 -4
- package/src/prompts/templates/UPDATES.md +8 -0
- package/src/providers/anthropic/client.ts +107 -219
- package/src/runtime/auth/route-policy.ts +23 -0
- package/src/runtime/http-server.ts +32 -2
- package/src/runtime/http-types.ts +12 -1
- package/src/runtime/migrations/vbundle-builder.ts +389 -3
- package/src/runtime/migrations/vbundle-importer.ts +8 -6
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
- package/src/runtime/routes/app-management-routes.ts +1 -11
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
- package/src/runtime/routes/archive-utils.ts +29 -0
- package/src/runtime/routes/avatar-routes.ts +2 -9
- package/src/runtime/routes/btw-routes.ts +14 -1
- package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
- package/src/runtime/routes/conversation-management-routes.ts +1 -14
- package/src/runtime/routes/conversation-query-routes.ts +49 -3
- package/src/runtime/routes/conversation-routes.ts +264 -44
- package/src/runtime/routes/heartbeat-routes.ts +4 -10
- package/src/runtime/routes/identity-routes.ts +53 -18
- package/src/runtime/routes/llm-context-normalization.ts +14 -10
- package/src/runtime/routes/log-export-routes.ts +23 -275
- package/src/runtime/routes/memory-item-routes.test.ts +168 -233
- package/src/runtime/routes/migration-routes.ts +18 -7
- package/src/runtime/routes/profiler-routes.ts +350 -0
- package/src/runtime/routes/schedule-routes.ts +27 -12
- package/src/runtime/routes/settings-routes.ts +95 -8
- package/src/runtime/routes/subagents-routes.ts +28 -7
- package/src/runtime/routes/user-route-dispatcher.ts +223 -0
- package/src/runtime/routes/user-routes.ts +41 -0
- package/src/runtime/routes/workspace-routes.ts +0 -1
- package/src/schedule/schedule-store.ts +30 -0
- package/src/schedule/scheduler.ts +45 -18
- package/src/skills/catalog-install.ts +10 -2
- package/src/skills/managed-store.ts +2 -2
- package/src/skills/skill-memory.ts +1 -293
- package/src/subagent/index.ts +13 -3
- package/src/subagent/manager.ts +308 -29
- package/src/subagent/types.ts +68 -0
- package/src/tasks/task-runner.ts +4 -4
- package/src/tools/apps/executors.ts +29 -4
- package/src/tools/filesystem/list.ts +93 -0
- package/src/tools/permission-checker.ts +78 -0
- package/src/tools/registry.ts +4 -0
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +1 -0
- package/src/tools/schedule/update.ts +6 -0
- package/src/tools/shared/filesystem/errors.ts +5 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
- package/src/tools/shared/filesystem/types.ts +17 -0
- package/src/tools/shared/shell-output.ts +31 -2
- package/src/tools/subagent/abort.ts +12 -2
- package/src/tools/subagent/message.ts +9 -2
- package/src/tools/subagent/notify-parent.ts +79 -0
- package/src/tools/subagent/read.ts +29 -8
- package/src/tools/subagent/resolve.ts +21 -0
- package/src/tools/subagent/spawn.ts +2 -0
- package/src/tools/subagent/status.ts +11 -1
- package/src/tools/system/avatar-generator.ts +3 -3
- package/src/tools/system/register.ts +23 -0
- package/src/tools/system/set-permission-mode.ts +103 -0
- package/src/tools/terminal/parser.ts +30 -5
- package/src/tools/terminal/safe-env.ts +16 -1
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +2 -0
- package/src/util/logger.ts +1 -1
- package/src/util/platform.ts +50 -17
- package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
- package/src/workspace/migrations/029-seed-pkb.ts +84 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/workspace/top-level-renderer.ts +5 -9
- package/src/__tests__/cli-memory.test.ts +0 -377
- package/src/__tests__/clipboard.test.ts +0 -88
- package/src/cli/cli-memory.ts +0 -179
- package/src/util/clipboard.ts +0 -34
package/src/subagent/manager.ts
CHANGED
|
@@ -23,7 +23,9 @@ import { getLogger } from "../util/logger.js";
|
|
|
23
23
|
import { getSandboxWorkingDir } from "../util/platform.js";
|
|
24
24
|
import {
|
|
25
25
|
SUBAGENT_LIMITS,
|
|
26
|
+
SUBAGENT_ROLE_REGISTRY,
|
|
26
27
|
type SubagentConfig,
|
|
28
|
+
type SubagentRole,
|
|
27
29
|
type SubagentState,
|
|
28
30
|
type SubagentStatus,
|
|
29
31
|
TERMINAL_STATUSES,
|
|
@@ -31,35 +33,76 @@ import {
|
|
|
31
33
|
|
|
32
34
|
const log = getLogger("subagent-manager");
|
|
33
35
|
|
|
36
|
+
/** How long to keep terminal subagent metadata after the live conversation is released (ms). */
|
|
37
|
+
const TERMINAL_RETENTION_MS = 30 * 60 * 1000; // 30 minutes
|
|
38
|
+
/** How often to sweep expired terminal entries (ms). */
|
|
39
|
+
const SWEEP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
40
|
+
|
|
41
|
+
// ── Skill ID merge helper ──────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Merge role-defined skill IDs with caller-provided skill IDs, deduplicating.
|
|
45
|
+
* Exported for direct unit testing.
|
|
46
|
+
*/
|
|
47
|
+
export function mergeSkillIds(
|
|
48
|
+
roleSkillIds: string[],
|
|
49
|
+
configSkillIds?: string[],
|
|
50
|
+
): string[] {
|
|
51
|
+
return [...new Set([...roleSkillIds, ...(configSkillIds ?? [])])];
|
|
52
|
+
}
|
|
53
|
+
|
|
34
54
|
// ── Default subagent system prompt ──────────────────────────────────────
|
|
35
55
|
|
|
36
|
-
function buildSubagentSystemPrompt(
|
|
56
|
+
function buildSubagentSystemPrompt(
|
|
57
|
+
config: SubagentConfig,
|
|
58
|
+
role: SubagentRole,
|
|
59
|
+
): string {
|
|
60
|
+
const roleConfig = SUBAGENT_ROLE_REGISTRY[role];
|
|
37
61
|
const sections: string[] = [
|
|
38
|
-
|
|
39
|
-
"Complete the task thoroughly and concisely.",
|
|
62
|
+
roleConfig.systemPromptPreamble,
|
|
40
63
|
"",
|
|
41
|
-
|
|
64
|
+
"## Your Task",
|
|
42
65
|
config.objective,
|
|
43
66
|
];
|
|
44
67
|
if (config.context) {
|
|
45
68
|
sections.push("", "## Context from Parent", config.context);
|
|
46
69
|
}
|
|
70
|
+
sections.push(
|
|
71
|
+
"",
|
|
72
|
+
"## Constraints",
|
|
73
|
+
`- Role: ${role}`,
|
|
74
|
+
"- You cannot spawn nested subagents.",
|
|
75
|
+
"- Use notify_parent to report important findings or if you are blocked.",
|
|
76
|
+
);
|
|
47
77
|
return sections.join("\n");
|
|
48
78
|
}
|
|
49
79
|
|
|
50
80
|
// ── Manager ─────────────────────────────────────────────────────────────
|
|
51
81
|
|
|
52
82
|
interface ManagedSubagent {
|
|
53
|
-
conversation
|
|
83
|
+
/** Live conversation — null after the subagent reaches a terminal state and is released. */
|
|
84
|
+
conversation: Conversation | null;
|
|
54
85
|
state: SubagentState;
|
|
55
86
|
/** Mutable reference to the parent's current sendToClient. Updated on reconnect. */
|
|
56
87
|
parentSendToClient: (msg: ServerMessage) => void;
|
|
88
|
+
/** Epoch ms after which this terminal entry can be removed by the TTL sweep. */
|
|
89
|
+
retainedUntil?: number;
|
|
90
|
+
/**
|
|
91
|
+
* Set to true when sendMessage enqueues a follow-up message while the
|
|
92
|
+
* initial objective loop is running. runAgentLoop fires drainQueue without
|
|
93
|
+
* awaiting it, and drainQueue shift()s the item synchronously — so both
|
|
94
|
+
* hasQueuedMessages() and isProcessing() can return false while the drain
|
|
95
|
+
* is still active. This flag lets the finally block in runSubagent defer
|
|
96
|
+
* the release to the TTL sweep rather than tearing down the conversation
|
|
97
|
+
* mid-drain.
|
|
98
|
+
*/
|
|
99
|
+
hadEnqueuedMessages?: boolean;
|
|
57
100
|
}
|
|
58
101
|
|
|
59
102
|
export interface SubagentNotificationInfo {
|
|
60
103
|
subagentId: string;
|
|
61
104
|
label: string;
|
|
62
|
-
status: "completed" | "failed" | "aborted";
|
|
105
|
+
status: "running" | "completed" | "failed" | "aborted";
|
|
63
106
|
error?: string;
|
|
64
107
|
conversationId?: string;
|
|
65
108
|
}
|
|
@@ -76,6 +119,8 @@ export class SubagentManager {
|
|
|
76
119
|
private subagents = new Map<string, ManagedSubagent>();
|
|
77
120
|
/** parentConversationId → Set<subagentId> */
|
|
78
121
|
private parentToChildren = new Map<string, Set<string>>();
|
|
122
|
+
/** `${parentConversationId}:${normalizedLabel}` → subagentId */
|
|
123
|
+
private labelIndex = new Map<string, string>();
|
|
79
124
|
|
|
80
125
|
/**
|
|
81
126
|
* Optional callback to inject a completion/failure message into the parent
|
|
@@ -119,10 +164,20 @@ export class SubagentManager {
|
|
|
119
164
|
);
|
|
120
165
|
}
|
|
121
166
|
|
|
167
|
+
// ── Resolve role ─────────────────────────────────────────────────
|
|
168
|
+
const role: SubagentRole = (config.role as SubagentRole) ?? "general";
|
|
169
|
+
if (!SUBAGENT_ROLE_REGISTRY[role]) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`Invalid subagent role "${config.role}". Must be one of: ${Object.keys(SUBAGENT_ROLE_REGISTRY).join(", ")}`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const roleConfig = SUBAGENT_ROLE_REGISTRY[role];
|
|
175
|
+
|
|
122
176
|
// ── Create conversation ─────────────────────────────────────────
|
|
123
177
|
const subagentId = uuid();
|
|
124
178
|
const conversationRecord = bootstrapConversation({
|
|
125
179
|
conversationType: "background",
|
|
180
|
+
source: "subagent",
|
|
126
181
|
origin: "subagent",
|
|
127
182
|
systemHint: `Subagent: ${config.label}`,
|
|
128
183
|
});
|
|
@@ -141,7 +196,7 @@ export class SubagentManager {
|
|
|
141
196
|
|
|
142
197
|
const systemPrompt =
|
|
143
198
|
config.systemPromptOverride ??
|
|
144
|
-
buildSubagentSystemPrompt({ ...config, id: subagentId });
|
|
199
|
+
buildSubagentSystemPrompt({ ...config, id: subagentId }, role);
|
|
145
200
|
const maxTokens = appConfig.maxTokens;
|
|
146
201
|
const workingDir = getSandboxWorkingDir();
|
|
147
202
|
|
|
@@ -190,14 +245,45 @@ export class SubagentManager {
|
|
|
190
245
|
workingDir,
|
|
191
246
|
this.broadcastToAllClients, // forward parent's broadcast so tool side-effects (e.g. app_files_changed) reach all clients
|
|
192
247
|
memoryPolicy,
|
|
248
|
+
undefined, // sharedCesClient
|
|
249
|
+
undefined, // speedOverride
|
|
250
|
+
"5m", // cacheTtl — subagents run tight tool-use loops, 5m is always hot
|
|
193
251
|
);
|
|
194
252
|
|
|
195
253
|
// Mark conversation as having no direct client — it routes through parent.
|
|
196
254
|
// This ensures interactive prompts (host attachment reads) fail fast.
|
|
197
255
|
conversation.updateClient(wrappedSendToClient, true);
|
|
256
|
+
conversation.setIsSubagent(true);
|
|
257
|
+
|
|
258
|
+
// Apply role-based tool filter if the role defines one.
|
|
259
|
+
if (roleConfig.allowedTools) {
|
|
260
|
+
conversation.setSubagentAllowedTools(new Set(roleConfig.allowedTools));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Pre-activate skills defined by the role config, merged with any caller-provided skill IDs.
|
|
264
|
+
const mergedSkillIds = mergeSkillIds(
|
|
265
|
+
roleConfig.skillIds,
|
|
266
|
+
config.preactivatedSkillIds,
|
|
267
|
+
);
|
|
268
|
+
if (mergedSkillIds.length > 0) {
|
|
269
|
+
conversation.setPreactivatedSkillIds(mergedSkillIds);
|
|
270
|
+
}
|
|
198
271
|
|
|
199
272
|
managed.conversation = conversation;
|
|
200
273
|
this.subagents.set(subagentId, managed);
|
|
274
|
+
const labelKey = `${config.parentConversationId}:${config.label.toLowerCase().trim()}`;
|
|
275
|
+
if (this.labelIndex.has(labelKey)) {
|
|
276
|
+
log.warn(
|
|
277
|
+
{
|
|
278
|
+
label: config.label,
|
|
279
|
+
parentConversationId: config.parentConversationId,
|
|
280
|
+
existingSubagentId: this.labelIndex.get(labelKey),
|
|
281
|
+
newSubagentId: subagentId,
|
|
282
|
+
},
|
|
283
|
+
"Label collision: new subagent overwrites label index entry (previous subagent still accessible by UUID)",
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
this.labelIndex.set(labelKey, subagentId);
|
|
201
287
|
|
|
202
288
|
// Track parent → child relationship.
|
|
203
289
|
if (!this.parentToChildren.has(config.parentConversationId)) {
|
|
@@ -240,25 +326,26 @@ export class SubagentManager {
|
|
|
240
326
|
const managed = this.subagents.get(subagentId);
|
|
241
327
|
if (!managed) return;
|
|
242
328
|
|
|
329
|
+
// Capture the live conversation — it is non-null at this point because
|
|
330
|
+
// spawn() sets it before firing runSubagent.
|
|
331
|
+
const conversation = managed.conversation!;
|
|
332
|
+
|
|
243
333
|
// Read the current parent sender so reconnects are picked up.
|
|
244
334
|
const getSender = () => managed.parentSendToClient;
|
|
245
335
|
|
|
246
336
|
this.setStatus(subagentId, "running", getSender());
|
|
247
337
|
managed.state.startedAt = Date.now();
|
|
248
338
|
|
|
249
|
-
const onEvent =
|
|
339
|
+
const onEvent = conversation.sendToClient;
|
|
250
340
|
|
|
251
341
|
try {
|
|
252
342
|
// Send the objective as the first user message and process it.
|
|
253
|
-
const messageId = await
|
|
254
|
-
|
|
255
|
-
[],
|
|
256
|
-
);
|
|
257
|
-
await managed.conversation.runAgentLoop(objective, messageId, onEvent);
|
|
343
|
+
const messageId = await conversation.persistUserMessage(objective, []);
|
|
344
|
+
await conversation.runAgentLoop(objective, messageId, onEvent);
|
|
258
345
|
|
|
259
346
|
// Agent loop completed successfully.
|
|
260
347
|
// Copy usage stats from the conversation before sending status (which includes usage).
|
|
261
|
-
managed.state.usage = { ...
|
|
348
|
+
managed.state.usage = { ...conversation.usageStats };
|
|
262
349
|
// Only update state + notify if still non-terminal (guards against abort race).
|
|
263
350
|
if (!TERMINAL_STATUSES.has(managed.state.status)) {
|
|
264
351
|
managed.state.completedAt = Date.now();
|
|
@@ -267,21 +354,40 @@ export class SubagentManager {
|
|
|
267
354
|
log.info({ subagentId }, "Subagent completed");
|
|
268
355
|
|
|
269
356
|
// Notify the parent conversation so the LLM can call subagent_read.
|
|
270
|
-
this.
|
|
357
|
+
this.notifyParentTerminal(managed, "completed", getSender());
|
|
271
358
|
}
|
|
272
359
|
} catch (err) {
|
|
273
360
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
274
361
|
managed.state.error = errorMsg;
|
|
275
362
|
managed.state.completedAt = Date.now();
|
|
276
|
-
|
|
363
|
+
// Copy usage from the captured conversation reference — managed.conversation
|
|
364
|
+
// may have been nulled by an external dispose() before catch runs.
|
|
365
|
+
managed.state.usage = { ...conversation.usageStats };
|
|
277
366
|
|
|
278
367
|
// Only update status if not already terminal (e.g. aborted).
|
|
279
368
|
if (!TERMINAL_STATUSES.has(managed.state.status)) {
|
|
280
369
|
this.setStatus(subagentId, "failed", getSender(), errorMsg);
|
|
281
|
-
this.
|
|
370
|
+
this.notifyParentTerminal(managed, "failed", getSender());
|
|
282
371
|
}
|
|
283
372
|
|
|
284
373
|
log.error({ subagentId, err }, "Subagent failed");
|
|
374
|
+
} finally {
|
|
375
|
+
// Release the heavyweight Conversation — output is already persisted in DB.
|
|
376
|
+
// runAgentLoop fires drainQueue without awaiting it, and drainQueue shift()s
|
|
377
|
+
// the next item synchronously — so both hasQueuedMessages() and
|
|
378
|
+
// isProcessing() can return false while a drain is still active. Use the
|
|
379
|
+
// hadEnqueuedMessages flag (set in sendMessage) to detect this case and
|
|
380
|
+
// defer the release to the TTL sweep rather than tearing down mid-drain.
|
|
381
|
+
if (managed.hadEnqueuedMessages) {
|
|
382
|
+
log.debug(
|
|
383
|
+
{ subagentId },
|
|
384
|
+
"Deferring conversation release — messages were enqueued during run",
|
|
385
|
+
);
|
|
386
|
+
managed.retainedUntil = Date.now() + TERMINAL_RETENTION_MS;
|
|
387
|
+
this.ensureSweepRunning();
|
|
388
|
+
} else {
|
|
389
|
+
this.releaseConversation(managed);
|
|
390
|
+
}
|
|
285
391
|
}
|
|
286
392
|
}
|
|
287
393
|
|
|
@@ -312,7 +418,7 @@ export class SubagentManager {
|
|
|
312
418
|
return false;
|
|
313
419
|
}
|
|
314
420
|
|
|
315
|
-
managed.conversation
|
|
421
|
+
managed.conversation?.abort();
|
|
316
422
|
managed.state.completedAt = Date.now();
|
|
317
423
|
if (parentSendToClient) {
|
|
318
424
|
// Route the status update through the stored parent sender so the
|
|
@@ -393,7 +499,8 @@ export class SubagentManager {
|
|
|
393
499
|
|
|
394
500
|
const managed = this.subagents.get(subagentId);
|
|
395
501
|
if (!managed) return "not_found";
|
|
396
|
-
if (TERMINAL_STATUSES.has(managed.state.status)
|
|
502
|
+
if (TERMINAL_STATUSES.has(managed.state.status) || !managed.conversation)
|
|
503
|
+
return "terminal";
|
|
397
504
|
|
|
398
505
|
const onEvent = managed.conversation.sendToClient;
|
|
399
506
|
const requestId = uuid();
|
|
@@ -408,13 +515,15 @@ export class SubagentManager {
|
|
|
408
515
|
if (result.rejected) {
|
|
409
516
|
return "sent"; // error event already delivered via onEvent
|
|
410
517
|
}
|
|
518
|
+
if (result.queued) {
|
|
519
|
+
managed.hadEnqueuedMessages = true;
|
|
520
|
+
}
|
|
411
521
|
if (!result.queued) {
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
managed.conversation
|
|
522
|
+
// Capture conversation before the await — managed.conversation may be
|
|
523
|
+
// nulled by an external dispose() while persistUserMessage is awaited.
|
|
524
|
+
const conversation = managed.conversation;
|
|
525
|
+
const messageId = await conversation.persistUserMessage(trimmed, []);
|
|
526
|
+
conversation
|
|
418
527
|
.runAgentLoop(trimmed, messageId, onEvent)
|
|
419
528
|
.catch((err) => {
|
|
420
529
|
log.error({ subagentId, err }, "Subagent message processing failed");
|
|
@@ -429,6 +538,15 @@ export class SubagentManager {
|
|
|
429
538
|
return this.subagents.get(subagentId)?.state;
|
|
430
539
|
}
|
|
431
540
|
|
|
541
|
+
getByLabel(
|
|
542
|
+
label: string,
|
|
543
|
+
parentConversationId: string,
|
|
544
|
+
): SubagentState | undefined {
|
|
545
|
+
const key = `${parentConversationId}:${label.toLowerCase().trim()}`;
|
|
546
|
+
const id = this.labelIndex.get(key);
|
|
547
|
+
return id ? this.getState(id) : undefined;
|
|
548
|
+
}
|
|
549
|
+
|
|
432
550
|
getChildrenOf(parentConversationId: string): SubagentState[] {
|
|
433
551
|
const children = this.parentToChildren.get(parentConversationId);
|
|
434
552
|
if (!children) return [];
|
|
@@ -465,6 +583,24 @@ export class SubagentManager {
|
|
|
465
583
|
|
|
466
584
|
// ── Cleanup ───────────────────────────────────────────────────────────
|
|
467
585
|
|
|
586
|
+
/**
|
|
587
|
+
* Release the live Conversation from a terminal subagent, keeping only
|
|
588
|
+
* lightweight metadata (state, config, usage) for later queries.
|
|
589
|
+
* The conversation's output is already persisted in the database.
|
|
590
|
+
*/
|
|
591
|
+
private releaseConversation(managed: ManagedSubagent): void {
|
|
592
|
+
if (!managed.conversation) return;
|
|
593
|
+
managed.conversation.dispose();
|
|
594
|
+
managed.conversation = null;
|
|
595
|
+
managed.retainedUntil = Date.now() + TERMINAL_RETENTION_MS;
|
|
596
|
+
this.ensureSweepRunning();
|
|
597
|
+
|
|
598
|
+
log.debug(
|
|
599
|
+
{ subagentId: managed.state.config.id },
|
|
600
|
+
"Released live conversation for terminal subagent",
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
468
604
|
/**
|
|
469
605
|
* Dispose a subagent and remove it from tracking.
|
|
470
606
|
* Should be called after the subagent reaches a terminal state
|
|
@@ -474,12 +610,24 @@ export class SubagentManager {
|
|
|
474
610
|
const managed = this.subagents.get(subagentId);
|
|
475
611
|
if (!managed) return;
|
|
476
612
|
|
|
477
|
-
if (
|
|
478
|
-
managed.
|
|
613
|
+
if (managed.conversation) {
|
|
614
|
+
if (!TERMINAL_STATUSES.has(managed.state.status)) {
|
|
615
|
+
managed.conversation.abort();
|
|
616
|
+
}
|
|
617
|
+
managed.conversation.dispose();
|
|
618
|
+
managed.conversation = null;
|
|
479
619
|
}
|
|
480
|
-
managed.conversation.dispose();
|
|
481
620
|
this.subagents.delete(subagentId);
|
|
482
621
|
|
|
622
|
+
// Remove from label index only if it still maps to this subagent
|
|
623
|
+
// (guards against stale delete when a newer subagent reused the label).
|
|
624
|
+
const label = managed.state.config.label;
|
|
625
|
+
const parentConvId = managed.state.config.parentConversationId;
|
|
626
|
+
const labelKey = `${parentConvId}:${label.toLowerCase().trim()}`;
|
|
627
|
+
if (this.labelIndex.get(labelKey) === subagentId) {
|
|
628
|
+
this.labelIndex.delete(labelKey);
|
|
629
|
+
}
|
|
630
|
+
|
|
483
631
|
// Remove from parent tracking.
|
|
484
632
|
const parentId = managed.state.config.parentConversationId;
|
|
485
633
|
const siblings = this.parentToChildren.get(parentId);
|
|
@@ -493,11 +641,71 @@ export class SubagentManager {
|
|
|
493
641
|
|
|
494
642
|
/** Dispose all subagents. Called on daemon shutdown. */
|
|
495
643
|
disposeAll(): void {
|
|
644
|
+
this.stopSweep();
|
|
496
645
|
for (const id of [...this.subagents.keys()]) {
|
|
497
646
|
this.dispose(id);
|
|
498
647
|
}
|
|
499
648
|
}
|
|
500
649
|
|
|
650
|
+
// ── TTL sweep for terminal metadata ──────────────────────────────────
|
|
651
|
+
|
|
652
|
+
private sweepTimer?: ReturnType<typeof setInterval>;
|
|
653
|
+
|
|
654
|
+
private ensureSweepRunning(): void {
|
|
655
|
+
if (this.sweepTimer) return;
|
|
656
|
+
this.sweepTimer = setInterval(
|
|
657
|
+
() => this.sweepTerminal(),
|
|
658
|
+
SWEEP_INTERVAL_MS,
|
|
659
|
+
);
|
|
660
|
+
// Don't let the sweep timer keep the process alive.
|
|
661
|
+
if (
|
|
662
|
+
this.sweepTimer &&
|
|
663
|
+
typeof this.sweepTimer === "object" &&
|
|
664
|
+
"unref" in this.sweepTimer
|
|
665
|
+
) {
|
|
666
|
+
(this.sweepTimer as { unref: () => void }).unref();
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
private stopSweep(): void {
|
|
671
|
+
if (this.sweepTimer) {
|
|
672
|
+
clearInterval(this.sweepTimer);
|
|
673
|
+
this.sweepTimer = undefined;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** Remove terminal entries whose retention period has expired. */
|
|
678
|
+
private sweepTerminal(): void {
|
|
679
|
+
const now = Date.now();
|
|
680
|
+
const expired: string[] = [];
|
|
681
|
+
for (const [id, managed] of this.subagents) {
|
|
682
|
+
if (!managed.retainedUntil || now < managed.retainedUntil) continue;
|
|
683
|
+
// If the retention window has expired and the conversation is still live,
|
|
684
|
+
// release it now — the drain has had ample time to complete.
|
|
685
|
+
if (managed.conversation) {
|
|
686
|
+
this.releaseConversation(managed);
|
|
687
|
+
// releaseConversation resets retainedUntil to keep metadata around for
|
|
688
|
+
// another window; the entry will be swept on the next pass.
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
expired.push(id);
|
|
692
|
+
}
|
|
693
|
+
for (const id of expired) {
|
|
694
|
+
log.debug(
|
|
695
|
+
{ subagentId: id },
|
|
696
|
+
"Sweeping expired terminal subagent metadata",
|
|
697
|
+
);
|
|
698
|
+
this.dispose(id);
|
|
699
|
+
}
|
|
700
|
+
// Stop the timer if there are no more entries to sweep.
|
|
701
|
+
const hasTerminal = [...this.subagents.values()].some(
|
|
702
|
+
(s) => s.retainedUntil !== undefined,
|
|
703
|
+
);
|
|
704
|
+
if (!hasTerminal) {
|
|
705
|
+
this.stopSweep();
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
501
709
|
// ── Internals ─────────────────────────────────────────────────────────
|
|
502
710
|
|
|
503
711
|
private setStatus(
|
|
@@ -529,11 +737,82 @@ export class SubagentManager {
|
|
|
529
737
|
} as ServerMessage);
|
|
530
738
|
}
|
|
531
739
|
|
|
740
|
+
// ── Child → Parent notification ────────────────────────────────────
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Look up the parent info for a child conversation.
|
|
744
|
+
* Returns undefined if the conversationId doesn't belong to a subagent.
|
|
745
|
+
*/
|
|
746
|
+
getParentInfo(childConversationId: string):
|
|
747
|
+
| {
|
|
748
|
+
parentConversationId: string;
|
|
749
|
+
subagentId: string;
|
|
750
|
+
label: string;
|
|
751
|
+
parentSendToClient: (msg: ServerMessage) => void;
|
|
752
|
+
}
|
|
753
|
+
| undefined {
|
|
754
|
+
for (const [subagentId, managed] of this.subagents) {
|
|
755
|
+
if (managed.state.conversationId === childConversationId) {
|
|
756
|
+
return {
|
|
757
|
+
parentConversationId: managed.state.config.parentConversationId,
|
|
758
|
+
subagentId,
|
|
759
|
+
label: managed.state.config.label,
|
|
760
|
+
parentSendToClient: managed.parentSendToClient,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return undefined;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Send a notification from a running subagent to its parent conversation.
|
|
769
|
+
* Returns true if the notification was sent, false if the child is not a
|
|
770
|
+
* subagent, is in a terminal state, or the parent callback is not wired.
|
|
771
|
+
*/
|
|
772
|
+
notifyParent(
|
|
773
|
+
childConversationId: string,
|
|
774
|
+
message: string,
|
|
775
|
+
urgency: string,
|
|
776
|
+
): boolean {
|
|
777
|
+
const info = this.getParentInfo(childConversationId);
|
|
778
|
+
if (!info) return false;
|
|
779
|
+
|
|
780
|
+
const managed = this.subagents.get(info.subagentId);
|
|
781
|
+
if (!managed || TERMINAL_STATUSES.has(managed.state.status)) return false;
|
|
782
|
+
if (!this.onSubagentFinished) return false;
|
|
783
|
+
|
|
784
|
+
let notificationString = `[Subagent "${info.label}" — ${urgency}] ${message}`;
|
|
785
|
+
if (urgency === "blocked") {
|
|
786
|
+
notificationString +=
|
|
787
|
+
"\nUse subagent_message to send guidance to this subagent.";
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
try {
|
|
791
|
+
this.onSubagentFinished(
|
|
792
|
+
info.parentConversationId,
|
|
793
|
+
notificationString,
|
|
794
|
+
info.parentSendToClient,
|
|
795
|
+
{
|
|
796
|
+
subagentId: info.subagentId,
|
|
797
|
+
label: info.label,
|
|
798
|
+
status: "running",
|
|
799
|
+
},
|
|
800
|
+
);
|
|
801
|
+
return true;
|
|
802
|
+
} catch (err) {
|
|
803
|
+
log.error(
|
|
804
|
+
{ subagentId: info.subagentId, err },
|
|
805
|
+
"Failed to notify parent from subagent",
|
|
806
|
+
);
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
532
811
|
/**
|
|
533
812
|
* Inject a completion/failure notification into the parent conversation
|
|
534
813
|
* so the LLM automatically informs the user.
|
|
535
814
|
*/
|
|
536
|
-
private
|
|
815
|
+
private notifyParentTerminal(
|
|
537
816
|
managed: ManagedSubagent,
|
|
538
817
|
outcome: "completed" | "failed",
|
|
539
818
|
parentSendToClient: (msg: ServerMessage) => void,
|
package/src/subagent/types.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface SubagentConfig {
|
|
|
41
41
|
preactivatedSkillIds?: string[];
|
|
42
42
|
/** Whether the parent should present the result to the user. Defaults to true. */
|
|
43
43
|
sendResultToUser?: boolean;
|
|
44
|
+
/** Optional role for the subagent. Defaults handled by consumers. */
|
|
45
|
+
role?: SubagentRole;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
// ── State (runtime) ─────────────────────────────────────────────────────
|
|
@@ -66,3 +68,69 @@ export const SUBAGENT_LIMITS = {
|
|
|
66
68
|
/** Max nesting depth (1 = no nested subagents). */
|
|
67
69
|
maxDepth: 1,
|
|
68
70
|
} as const;
|
|
71
|
+
|
|
72
|
+
// ── Roles ───────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export type SubagentRole = "general" | "researcher" | "coder" | "planner";
|
|
75
|
+
|
|
76
|
+
export interface SubagentRoleConfig {
|
|
77
|
+
/**
|
|
78
|
+
* When defined, only these tools are visible to the subagent.
|
|
79
|
+
* `undefined` means no filter (all tools available).
|
|
80
|
+
*/
|
|
81
|
+
allowedTools?: string[];
|
|
82
|
+
/** Skill IDs to pre-activate on the subagent conversation. */
|
|
83
|
+
skillIds: string[];
|
|
84
|
+
/** Role-specific text prepended to the subagent system prompt. */
|
|
85
|
+
systemPromptPreamble: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const SUBAGENT_ROLE_REGISTRY: Record<SubagentRole, SubagentRoleConfig> =
|
|
89
|
+
{
|
|
90
|
+
general: {
|
|
91
|
+
allowedTools: undefined,
|
|
92
|
+
skillIds: [],
|
|
93
|
+
systemPromptPreamble:
|
|
94
|
+
"You are a general-purpose subagent. Complete the delegated task thoroughly and concisely.",
|
|
95
|
+
},
|
|
96
|
+
researcher: {
|
|
97
|
+
allowedTools: [
|
|
98
|
+
"web_search",
|
|
99
|
+
"web_fetch",
|
|
100
|
+
"file_read",
|
|
101
|
+
"file_list",
|
|
102
|
+
"recall",
|
|
103
|
+
"notify_parent",
|
|
104
|
+
],
|
|
105
|
+
skillIds: [],
|
|
106
|
+
systemPromptPreamble:
|
|
107
|
+
"You are a research-focused subagent with read-only access. Search the web, read files, and recall memories. You cannot write files or run shell commands.",
|
|
108
|
+
},
|
|
109
|
+
coder: {
|
|
110
|
+
allowedTools: [
|
|
111
|
+
"bash",
|
|
112
|
+
"file_read",
|
|
113
|
+
"file_write",
|
|
114
|
+
"file_edit",
|
|
115
|
+
"web_search",
|
|
116
|
+
"recall",
|
|
117
|
+
"notify_parent",
|
|
118
|
+
],
|
|
119
|
+
skillIds: [],
|
|
120
|
+
systemPromptPreamble:
|
|
121
|
+
"You are a code-focused subagent with file and shell access. Read, write, and edit files, and run shell commands.",
|
|
122
|
+
},
|
|
123
|
+
planner: {
|
|
124
|
+
allowedTools: [
|
|
125
|
+
"file_read",
|
|
126
|
+
"file_list",
|
|
127
|
+
"web_search",
|
|
128
|
+
"web_fetch",
|
|
129
|
+
"recall",
|
|
130
|
+
"notify_parent",
|
|
131
|
+
],
|
|
132
|
+
skillIds: [],
|
|
133
|
+
systemPromptPreamble:
|
|
134
|
+
"You are an analysis-focused subagent with read-only access. Read files, search the web, and synthesize findings. You cannot write files or run shell commands.",
|
|
135
|
+
},
|
|
136
|
+
};
|
package/src/tasks/task-runner.ts
CHANGED
|
@@ -60,10 +60,10 @@ export async function runTask(
|
|
|
60
60
|
|
|
61
61
|
const run = createTaskRun(task.id);
|
|
62
62
|
const conversation = bootstrapConversation({
|
|
63
|
-
// Schedule-triggered tasks use "
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
conversationType: opts.source === "schedule" ?
|
|
63
|
+
// Schedule-triggered tasks use "scheduled" so they don't crowd out
|
|
64
|
+
// interactive conversations in the main list; non-schedule tasks use
|
|
65
|
+
// "background" to stay out of the list entirely.
|
|
66
|
+
conversationType: opts.source === "schedule" ? "scheduled" : "background",
|
|
67
67
|
source: opts.source === "schedule" ? "schedule" : "task",
|
|
68
68
|
scheduleJobId: opts.scheduleJobId,
|
|
69
69
|
groupId:
|
|
@@ -277,10 +277,10 @@ export interface AppRefreshInput {
|
|
|
277
277
|
app_id: string;
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
export function executeAppRefresh(
|
|
280
|
+
export async function executeAppRefresh(
|
|
281
281
|
input: AppRefreshInput,
|
|
282
282
|
store: AppStore,
|
|
283
|
-
): ExecutorResult {
|
|
283
|
+
): Promise<ExecutorResult> {
|
|
284
284
|
const app = store.getApp(input.app_id);
|
|
285
285
|
if (!app) {
|
|
286
286
|
return {
|
|
@@ -289,9 +289,34 @@ export function executeAppRefresh(
|
|
|
289
289
|
};
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
// Empty update bumps updatedAt timestamp, triggering
|
|
293
|
-
//
|
|
292
|
+
// Empty update bumps updatedAt timestamp, triggering surface refresh on
|
|
293
|
+
// the client side.
|
|
294
294
|
const updated = store.updateApp(input.app_id, {});
|
|
295
|
+
|
|
296
|
+
// Multifile apps need an explicit compile so the LLM sees any errors
|
|
297
|
+
// (bad imports, syntax issues, etc.) instead of silently serving the
|
|
298
|
+
// stale scaffold placeholder from the initial app_create.
|
|
299
|
+
if (app.formatVersion === 2) {
|
|
300
|
+
const appDir = getAppDirPath(input.app_id);
|
|
301
|
+
const compileResult = await compileApp(appDir);
|
|
302
|
+
return {
|
|
303
|
+
content: JSON.stringify({
|
|
304
|
+
refreshed: true,
|
|
305
|
+
appId: updated.id,
|
|
306
|
+
name: updated.name,
|
|
307
|
+
compiled: compileResult.ok,
|
|
308
|
+
...(compileResult.ok
|
|
309
|
+
? { compile_duration_ms: compileResult.durationMs }
|
|
310
|
+
: {
|
|
311
|
+
compile_errors: compileResult.errors,
|
|
312
|
+
compile_warnings: compileResult.warnings,
|
|
313
|
+
compile_duration_ms: compileResult.durationMs,
|
|
314
|
+
}),
|
|
315
|
+
}),
|
|
316
|
+
isError: false,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
295
320
|
return {
|
|
296
321
|
content: JSON.stringify({
|
|
297
322
|
refreshed: true,
|