@swarmclawai/swarmclaw 1.9.14 → 1.9.16

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 CHANGED
@@ -399,6 +399,24 @@ Operational docs: https://swarmclaw.ai/docs/observability
399
399
 
400
400
  ## Releases
401
401
 
402
+ ### v1.9.16 Highlights
403
+
404
+ Agent planning controls release: strict planning is now a first-class agent setting instead of a hidden persisted field, so operators can decide which agents must expose machine-readable plans before multi-step work.
405
+
406
+ - **Agent editor control.** Advanced agent settings now include a Standard / Strict planning selector with inline behavior guidance.
407
+ - **Runtime prompt wiring.** Strict planning continues to inject the existing `[MAIN_LOOP_PLAN]` contract before multi-step tool work, and the test suite now keeps that prompt section in the runtime gate.
408
+ - **Portable agent packs.** Agent exports preserve `planningMode`, so planning discipline follows agents across installs.
409
+ - **API coverage.** Agent create and update route tests verify that strict planning persists without clobbering unrelated settings.
410
+
411
+ ### v1.9.15 Highlights
412
+
413
+ Run handoff release: SwarmClaw now turns completed, failed, queued, or running execution records into copyable handoff packets with outcome, evidence, artifacts, timeline, usage, resume commands, and recommended next actions.
414
+
415
+ - **Run handoff API.** `GET /api/runs/:id/handoff` returns structured handoff JSON, and `?format=markdown` returns copyable markdown.
416
+ - **Run Review copy action.** The run detail sheet exposes a copy handoff button so operators can move outcome evidence into another session without replaying the full event log.
417
+ - **CLI access.** `swarmclaw runs handoff <runId> --query format=markdown` exposes the same packet for scripts and release automation.
418
+ - **Readiness guidance.** Packets mark failed, cancelled, running, warning, or under-evidenced runs as blocked or needing attention before another operator relies on the result.
419
+
402
420
  ### v1.9.14 Highlights
403
421
 
404
422
  Session context-pack release: SwarmClaw now turns a live chat into a concise handoff packet with session metadata, recent visible turns, linked tasks, attachments, resume handles, and next actions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.9.14",
3
+ "version": "1.9.16",
4
4
  "description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
5
5
  "main": "electron-dist/main.js",
6
6
  "license": "MIT",
@@ -88,7 +88,7 @@
88
88
  "test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
89
89
  "test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
90
90
  "test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/gateways/gateway-topology.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/gateways/topology-route.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
91
- "test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
91
+ "test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
92
92
  "test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
93
93
  "test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
94
94
  "test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
@@ -101,6 +101,27 @@ test('POST /api/agents accepts a valid provider and creates the agent', async ()
101
101
  saveAgents(agents)
102
102
  })
103
103
 
104
+ test('POST /api/agents persists strict planning mode for created agents', async () => {
105
+ const response = await createAgent(new Request('http://local/api/agents', {
106
+ method: 'POST',
107
+ headers: { 'content-type': 'application/json' },
108
+ body: JSON.stringify({
109
+ name: 'Planning Agent',
110
+ provider: 'ollama',
111
+ model: 'qwen3.5',
112
+ planningMode: 'strict',
113
+ }),
114
+ }))
115
+
116
+ assert.equal(response.status, 200)
117
+ const body = await response.json()
118
+ assert.equal(body.planningMode, 'strict')
119
+
120
+ const agents = loadAgents()
121
+ delete agents[body.id]
122
+ saveAgents(agents)
123
+ })
124
+
104
125
  test('POST /api/agents rejects missing required fields with a 400', async () => {
105
126
  const response = await createAgent(new Request('http://local/api/agents', {
106
127
  method: 'POST',
@@ -163,6 +184,28 @@ test('PUT /api/agents/:id does not clobber untouched fields with schema defaults
163
184
  assert.equal(body.proactiveMemory, false)
164
185
  })
165
186
 
187
+ test('PUT /api/agents/:id updates planning mode without clobbering other fields', async () => {
188
+ seedAgent('agent-planning-mode', {
189
+ name: 'Planner',
190
+ tools: ['memory'],
191
+ planningMode: 'off',
192
+ proactiveMemory: false,
193
+ })
194
+
195
+ const response = await putAgent(new Request('http://local/api/agents/agent-planning-mode', {
196
+ method: 'PUT',
197
+ headers: { 'content-type': 'application/json' },
198
+ body: JSON.stringify({ planningMode: 'strict' }),
199
+ }), routeParams('agent-planning-mode'))
200
+
201
+ assert.equal(response.status, 200)
202
+ const body = await response.json()
203
+ assert.equal(body.planningMode, 'strict')
204
+ assert.equal(body.name, 'Planner')
205
+ assert.deepEqual(body.tools, ['memory'])
206
+ assert.equal(body.proactiveMemory, false)
207
+ })
208
+
166
209
  test('PUT /api/agents/:id rejects non-string name', async () => {
167
210
  seedAgent('agent-bad-name', { name: 'Good' })
168
211
 
@@ -0,0 +1,26 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { listEvidenceArtifacts } from '@/lib/server/artifacts/artifact-resolver'
3
+ import { buildRunBrief } from '@/lib/server/runs/run-brief'
4
+ import { buildRunHandoffPacket, formatRunHandoffMarkdown } from '@/lib/server/runs/run-handoff'
5
+ import { getUnifiedRunById, listUnifiedRunEvents } from '@/lib/server/runs/unified-run-queries'
6
+
7
+ export const dynamic = 'force-dynamic'
8
+
9
+ export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
10
+ const { id } = await params
11
+ const run = getUnifiedRunById(id)
12
+ if (!run) return NextResponse.json({ error: 'Run not found' }, { status: 404 })
13
+
14
+ const events = listUnifiedRunEvents(id, 300)
15
+ const brief = buildRunBrief(run, events)
16
+ const packet = buildRunHandoffPacket(run, brief, listEvidenceArtifacts({ runId: id }))
17
+ const url = new URL(req.url)
18
+
19
+ if (url.searchParams.get('format') === 'markdown') {
20
+ return new NextResponse(formatRunHandoffMarkdown(packet), {
21
+ headers: { 'content-type': 'text/markdown; charset=utf-8' },
22
+ })
23
+ }
24
+
25
+ return NextResponse.json(packet)
26
+ }
@@ -0,0 +1,120 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+
4
+ import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
5
+
6
+ test('GET /api/runs/[id]/handoff returns structured and markdown handoff context', () => {
7
+ const output = runWithTempDataDir<{
8
+ status: number
9
+ markdownStatus: number
10
+ missingStatus: number
11
+ schemaVersion: number
12
+ readiness: string
13
+ artifactCount: number
14
+ markdownContentType: string
15
+ markdownIncludesTitle: boolean
16
+ markdownIncludesCommand: boolean
17
+ }>(`
18
+ const storageMod = await import('./src/lib/server/storage')
19
+ const ledgerMod = await import('./src/lib/server/runtime/run-ledger')
20
+ const routeMod = await import('./src/app/api/runs/[id]/handoff/route')
21
+ const storage = storageMod.default || storageMod
22
+ const ledger = ledgerMod.default || ledgerMod
23
+ const route = routeMod.default || routeMod
24
+
25
+ const now = Date.now()
26
+ storage.saveSessions({
27
+ sess_run_handoff: {
28
+ id: 'sess_run_handoff',
29
+ name: 'Run handoff session',
30
+ cwd: process.env.WORKSPACE_DIR,
31
+ user: 'tester',
32
+ provider: 'openai',
33
+ model: 'gpt-4o-mini',
34
+ messages: [],
35
+ createdAt: now,
36
+ lastActiveAt: now,
37
+ },
38
+ })
39
+
40
+ ledger.persistRun({
41
+ id: 'run_handoff_1',
42
+ sessionId: 'sess_run_handoff',
43
+ source: 'task',
44
+ internal: false,
45
+ mode: 'direct',
46
+ status: 'completed',
47
+ messagePreview: 'Ship the next fix',
48
+ queuedAt: now - 5000,
49
+ startedAt: now - 4000,
50
+ endedAt: now - 1000,
51
+ resultPreview: 'Fix shipped and verified.',
52
+ totalInputTokens: 11,
53
+ totalOutputTokens: 22,
54
+ retrievalSummary: { citationCount: 1, sourceIds: ['source_1'] },
55
+ })
56
+ ledger.appendPersistedRunEvent({
57
+ runId: 'run_handoff_1',
58
+ sessionId: 'sess_run_handoff',
59
+ phase: 'status',
60
+ status: 'completed',
61
+ timestamp: now - 1000,
62
+ event: { t: 'md', text: 'Fix shipped and verified.' },
63
+ citations: [{
64
+ sourceId: 'source_1',
65
+ sourceTitle: 'Release evidence',
66
+ sourceKind: 'manual',
67
+ sourceUrl: 'https://example.test/release',
68
+ sourceLabel: null,
69
+ chunkId: 'chunk_1',
70
+ chunkIndex: 0,
71
+ chunkCount: 1,
72
+ charStart: 0,
73
+ charEnd: 20,
74
+ sectionLabel: null,
75
+ snippet: 'Release checks passed.',
76
+ whyMatched: null,
77
+ score: 0.9,
78
+ }],
79
+ })
80
+
81
+ const response = await route.GET(
82
+ new Request('http://local/api/runs/run_handoff_1/handoff'),
83
+ { params: Promise.resolve({ id: 'run_handoff_1' }) },
84
+ )
85
+ const payload = await response.json()
86
+
87
+ const markdownResponse = await route.GET(
88
+ new Request('http://local/api/runs/run_handoff_1/handoff?format=markdown'),
89
+ { params: Promise.resolve({ id: 'run_handoff_1' }) },
90
+ )
91
+ const markdown = await markdownResponse.text()
92
+
93
+ const missingResponse = await route.GET(
94
+ new Request('http://local/api/runs/missing/handoff'),
95
+ { params: Promise.resolve({ id: 'missing' }) },
96
+ )
97
+
98
+ console.log(JSON.stringify({
99
+ status: response.status,
100
+ markdownStatus: markdownResponse.status,
101
+ missingStatus: missingResponse.status,
102
+ schemaVersion: payload.schemaVersion,
103
+ readiness: payload.readiness.status,
104
+ artifactCount: payload.artifacts.length,
105
+ markdownContentType: markdownResponse.headers.get('content-type') || '',
106
+ markdownIncludesTitle: markdown.includes('# Run Handoff: Ship the next fix'),
107
+ markdownIncludesCommand: markdown.includes('swarmclaw runs handoff run_handoff_1'),
108
+ }))
109
+ `, { prefix: 'swarmclaw-run-handoff-route-' })
110
+
111
+ assert.equal(output.status, 200)
112
+ assert.equal(output.markdownStatus, 200)
113
+ assert.equal(output.missingStatus, 404)
114
+ assert.equal(output.schemaVersion, 1)
115
+ assert.equal(output.readiness, 'ready')
116
+ assert.equal(output.artifactCount >= 1, true)
117
+ assert.match(output.markdownContentType, /text\/markdown/)
118
+ assert.equal(output.markdownIncludesTitle, true)
119
+ assert.equal(output.markdownIncludesCommand, true)
120
+ })
package/src/cli/index.js CHANGED
@@ -572,6 +572,7 @@ const COMMAND_GROUPS = [
572
572
  cmd('get', 'GET', '/runs/:id', 'Get run by id'),
573
573
  cmd('events', 'GET', '/runs/:id/events', 'Get run event history by run id'),
574
574
  cmd('brief', 'GET', '/runs/:id/brief', 'Get deterministic run brief by run id'),
575
+ cmd('handoff', 'GET', '/runs/:id/handoff', 'Get run handoff packet by run id'),
575
576
  ],
576
577
  },
577
578
  {
package/src/cli/spec.js CHANGED
@@ -552,6 +552,7 @@ const COMMAND_GROUPS = {
552
552
  get: { description: 'Get run by id', method: 'GET', path: '/runs/:id', params: ['id'] },
553
553
  events: { description: 'Get run event history by run id', method: 'GET', path: '/runs/:id/events', params: ['id'] },
554
554
  brief: { description: 'Get deterministic run brief by run id', method: 'GET', path: '/runs/:id/brief', params: ['id'] },
555
+ handoff: { description: 'Get run handoff packet by run id', method: 'GET', path: '/runs/:id/handoff', params: ['id'] },
555
556
  },
556
557
  },
557
558
  webhooks: {
@@ -26,6 +26,7 @@ import { StatusDot } from '@/components/ui/status-dot'
26
26
  import { resolveStoredOllamaMode } from '@/lib/ollama-mode'
27
27
  import { errorMessage } from '@/lib/shared-utils'
28
28
  import { getDefaultAgentToolIds } from '@/lib/agent-default-tools'
29
+ import { AGENT_PLANNING_MODE_OPTIONS, describeAgentPlanningMode, normalizeAgentPlanningMode, type AgentPlanningMode } from '@/lib/agent-planning-mode'
29
30
  import { getEnabledExtensionIds, getEnabledToolIds } from '@/lib/capability-selection'
30
31
  import { buildAgentSelectableProviders, resolveAgentSelectableProviderCredentials } from '@/lib/agent-provider-options'
31
32
  import { AgentSocialSettings } from '@/features/swarmfeed/agent-social-settings'
@@ -238,6 +239,7 @@ export function AgentSheet() {
238
239
  const [memoryTierPreference, setMemoryTierPreference] = useState<'working' | 'durable' | 'archive' | 'blended'>('blended')
239
240
  const [proactiveMemory, setProactiveMemory] = useState(true)
240
241
  const [autoDraftSkillSuggestions, setAutoDraftSkillSuggestions] = useState(true)
242
+ const [planningMode, setPlanningMode] = useState<AgentPlanningMode>('off')
241
243
  const [autoRecovery, setAutoRecovery] = useState(false)
242
244
  const [disabled, setDisabled] = useState(false)
243
245
  const [filesystemScope, setFilesystemScope] = useState<'workspace' | 'machine'>('workspace')
@@ -444,6 +446,7 @@ export function AgentSheet() {
444
446
  setMemoryTierPreference(editing.memoryTierPreference || 'blended')
445
447
  setProactiveMemory(editing.proactiveMemory !== false)
446
448
  setAutoDraftSkillSuggestions(editing.autoDraftSkillSuggestions !== false)
449
+ setPlanningMode(normalizeAgentPlanningMode(editing.planningMode))
447
450
  setAutoRecovery(editing.autoRecovery || false)
448
451
  setDisabled(editing.disabled === true)
449
452
  setFilesystemScope(editing.filesystemScope === 'machine' ? 'machine' : 'workspace')
@@ -527,6 +530,7 @@ export function AgentSheet() {
527
530
  setMemoryTierPreference(src.memoryTierPreference || 'blended')
528
531
  setProactiveMemory(src.proactiveMemory !== false)
529
532
  setAutoDraftSkillSuggestions(src.autoDraftSkillSuggestions !== false)
533
+ setPlanningMode(normalizeAgentPlanningMode(src.planningMode))
530
534
  setAutoRecovery(src.autoRecovery || false)
531
535
  setDisabled(false)
532
536
  setFilesystemScope(src.filesystemScope === 'machine' ? 'machine' : 'workspace')
@@ -602,6 +606,7 @@ export function AgentSheet() {
602
606
  setMemoryTierPreference('blended')
603
607
  setProactiveMemory(true)
604
608
  setAutoDraftSkillSuggestions(true)
609
+ setPlanningMode('off')
605
610
  setAutoRecovery(false)
606
611
  setDisabled(false)
607
612
  setVoiceId('')
@@ -809,6 +814,7 @@ export function AgentSheet() {
809
814
  memoryTierPreference,
810
815
  proactiveMemory,
811
816
  autoDraftSkillSuggestions,
817
+ planningMode,
812
818
  autoRecovery,
813
819
  disabled,
814
820
  filesystemScope: filesystemScope === 'machine' ? 'machine' as const : undefined,
@@ -916,6 +922,7 @@ export function AgentSheet() {
916
922
  extensions: getEnabledExtensionIds(editing),
917
923
  capabilities: editing.capabilities,
918
924
  elevenLabsVoiceId: editing.elevenLabsVoiceId || null,
925
+ planningMode: normalizeAgentPlanningMode(editing.planningMode),
919
926
  soul: editing.soul,
920
927
  systemPrompt: editing.systemPrompt,
921
928
  }],
@@ -1043,6 +1050,7 @@ export function AgentSheet() {
1043
1050
  if (projectId) badges.push('Project')
1044
1051
  if (thinkingLevel) badges.push('Thinking')
1045
1052
  if (!autoDraftSkillSuggestions) badges.push('Skill drafting')
1053
+ if (planningMode === 'strict') badges.push('Planning')
1046
1054
  return Array.from(new Set(badges))
1047
1055
  }, [
1048
1056
  autoDraftSkillSuggestions,
@@ -1061,6 +1069,7 @@ export function AgentSheet() {
1061
1069
  memoryScopeMode,
1062
1070
  memoryTierPreference,
1063
1071
  proactiveMemory,
1072
+ planningMode,
1064
1073
  projectId,
1065
1074
  routingStrategy,
1066
1075
  routingTargets.length,
@@ -2145,7 +2154,15 @@ export function AgentSheet() {
2145
2154
  <option value="durable">Durable memory</option>
2146
2155
  <option value="archive">Archive memory</option>
2147
2156
  </select>
2157
+ <select value={planningMode} onChange={(e) => setPlanningMode(normalizeAgentPlanningMode(e.target.value))} className={inputClass}>
2158
+ {AGENT_PLANNING_MODE_OPTIONS.map((option) => (
2159
+ <option key={option.value} value={option.value}>{option.label}</option>
2160
+ ))}
2161
+ </select>
2148
2162
  </div>
2163
+ <p className="mb-4 text-[12px] leading-[1.6] text-text-3/70">
2164
+ {describeAgentPlanningMode(planningMode)}
2165
+ </p>
2149
2166
  <div className="space-y-3">
2150
2167
  <label className="flex items-center gap-3 cursor-pointer">
2151
2168
  <div
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { useEffect, useMemo, useState, useCallback } from 'react'
4
+ import { ClipboardList } from 'lucide-react'
4
5
  import { api } from '@/lib/app/api-client'
5
6
  import { useNow } from '@/hooks/use-now'
6
7
  import { useWs } from '@/hooks/use-ws'
@@ -10,6 +11,7 @@ import type { EvidenceArtifact, RunBrief, RunEventRecord, SessionRunRecord, Sess
10
11
  import { PageLoader } from '@/components/ui/page-loader'
11
12
  import { formatElapsed } from '@/lib/format-display'
12
13
  import { GroundingPanel } from '@/components/knowledge/grounding-panel'
14
+ import { copyTextToClipboard } from '@/lib/clipboard'
13
15
 
14
16
  const STATUS_COLORS: Record<SessionRunStatus, { bg: string; text: string }> = {
15
17
  queued: { bg: 'bg-yellow-500/10', text: 'text-yellow-400' },
@@ -47,6 +49,9 @@ export function RunList() {
47
49
  const [eventsLoading, setEventsLoading] = useState(false)
48
50
  const [briefLoading, setBriefLoading] = useState(false)
49
51
  const [artifactsLoading, setArtifactsLoading] = useState(false)
52
+ const [handoffCopying, setHandoffCopying] = useState(false)
53
+ const [handoffCopied, setHandoffCopied] = useState(false)
54
+ const [handoffError, setHandoffError] = useState<string | null>(null)
50
55
 
51
56
  const fetchRuns = useCallback(async () => {
52
57
  try {
@@ -57,7 +62,6 @@ export function RunList() {
57
62
  }, [])
58
63
 
59
64
  useEffect(() => {
60
- // eslint-disable-next-line react-hooks/set-state-in-effect
61
65
  fetchRuns()
62
66
  }, [fetchRuns])
63
67
 
@@ -102,8 +106,30 @@ export function RunList() {
102
106
  setEventsLoading(true)
103
107
  setBriefLoading(true)
104
108
  setArtifactsLoading(true)
109
+ setHandoffCopied(false)
110
+ setHandoffError(null)
105
111
  }, [])
106
112
 
113
+ const copyRunHandoff = useCallback(async () => {
114
+ if (!selected || handoffCopying) return
115
+ setHandoffCopying(true)
116
+ setHandoffError(null)
117
+ try {
118
+ const markdown = await api<string>('GET', `/runs/${selected.id}/handoff?format=markdown`)
119
+ const copied = await copyTextToClipboard(markdown)
120
+ if (!copied) {
121
+ setHandoffError('Clipboard unavailable.')
122
+ return
123
+ }
124
+ setHandoffCopied(true)
125
+ setTimeout(() => setHandoffCopied(false), 2000)
126
+ } catch {
127
+ setHandoffError('Could not copy handoff.')
128
+ } finally {
129
+ setHandoffCopying(false)
130
+ }
131
+ }, [handoffCopying, selected])
132
+
107
133
  const sources = useMemo(() => {
108
134
  return Array.from(new Set(runs.map((run) => run.source).filter(Boolean))).sort((a, b) => a.localeCompare(b))
109
135
  }, [runs])
@@ -248,12 +274,24 @@ export function RunList() {
248
274
  {selected && (
249
275
  <div style={{ animation: 'fade-in 0.3s ease' }}>
250
276
  <div className="mb-6">
251
- <div className="flex items-center gap-3 mb-3">
252
- <span className={`text-[11px] font-700 uppercase tracking-wider px-2.5 py-1 rounded-[6px] ${STATUS_COLORS[selected.status].bg} ${STATUS_COLORS[selected.status].text}`}>
253
- {selected.status}
254
- </span>
255
- <span className="text-[12px] font-mono text-text-3/60">{selected.source}</span>
277
+ <div className="mb-3 flex flex-wrap items-center gap-3">
278
+ <div className="flex min-w-0 items-center gap-3">
279
+ <span className={`text-[11px] font-700 uppercase tracking-wider px-2.5 py-1 rounded-[6px] ${STATUS_COLORS[selected.status].bg} ${STATUS_COLORS[selected.status].text}`}>
280
+ {selected.status}
281
+ </span>
282
+ <span className="text-[12px] font-mono text-text-3/60">{selected.source}</span>
283
+ </div>
284
+ <button
285
+ type="button"
286
+ onClick={copyRunHandoff}
287
+ disabled={handoffCopying}
288
+ className="ml-auto inline-flex items-center gap-1.5 rounded-[8px] border border-white/[0.07] bg-white/[0.04] px-2.5 py-1.5 text-[11px] font-700 text-text-2 transition-colors hover:bg-white/[0.07] disabled:cursor-not-allowed disabled:opacity-60"
289
+ >
290
+ <ClipboardList size={13} />
291
+ {handoffCopied ? 'Copied' : handoffCopying ? 'Copying...' : 'Copy Handoff'}
292
+ </button>
256
293
  </div>
294
+ {handoffError && <p className="mb-3 text-[11px] font-600 text-red-400">{handoffError}</p>}
257
295
  <h2 className="font-display text-[20px] font-700 tracking-[-0.02em] mb-2 leading-snug">
258
296
  Run Details
259
297
  </h2>
@@ -0,0 +1,32 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+
4
+ import {
5
+ AGENT_PLANNING_MODE_OPTIONS,
6
+ describeAgentPlanningMode,
7
+ isAgentPlanningModeEnabled,
8
+ normalizeAgentPlanningMode,
9
+ } from './agent-planning-mode'
10
+
11
+ test('normalizeAgentPlanningMode accepts only supported persisted values', () => {
12
+ assert.equal(normalizeAgentPlanningMode('strict'), 'strict')
13
+ assert.equal(normalizeAgentPlanningMode('off'), 'off')
14
+ assert.equal(normalizeAgentPlanningMode(null), 'off')
15
+ assert.equal(normalizeAgentPlanningMode(undefined), 'off')
16
+ assert.equal(normalizeAgentPlanningMode('unexpected'), 'off')
17
+ })
18
+
19
+ test('planning mode options include a safe default and strict mode', () => {
20
+ assert.deepEqual(
21
+ AGENT_PLANNING_MODE_OPTIONS.map((option) => option.value),
22
+ ['off', 'strict'],
23
+ )
24
+ assert.equal(isAgentPlanningModeEnabled('strict'), true)
25
+ assert.equal(isAgentPlanningModeEnabled('off'), false)
26
+ assert.equal(isAgentPlanningModeEnabled(null), false)
27
+ })
28
+
29
+ test('describeAgentPlanningMode returns operator-facing copy for each mode', () => {
30
+ assert.match(describeAgentPlanningMode('off'), /No extra plan contract/)
31
+ assert.match(describeAgentPlanningMode('strict'), /machine-readable plan block/)
32
+ })
@@ -0,0 +1,34 @@
1
+ import type { Agent } from '@/types'
2
+
3
+ export type AgentPlanningMode = NonNullable<Agent['planningMode']>
4
+
5
+ export const AGENT_PLANNING_MODE_OPTIONS: ReadonlyArray<{
6
+ value: AgentPlanningMode
7
+ label: string
8
+ description: string
9
+ }> = [
10
+ {
11
+ value: 'off',
12
+ label: 'Standard',
13
+ description: 'No extra plan contract. The agent can answer, plan, or act normally based on the task.',
14
+ },
15
+ {
16
+ value: 'strict',
17
+ label: 'Strict planning',
18
+ description: 'Require a machine-readable plan block before multi-step tool work so progress can be tracked.',
19
+ },
20
+ ]
21
+
22
+ export function normalizeAgentPlanningMode(value: unknown): AgentPlanningMode {
23
+ return value === 'strict' ? 'strict' : 'off'
24
+ }
25
+
26
+ export function isAgentPlanningModeEnabled(value: Agent['planningMode'] | undefined): boolean {
27
+ return normalizeAgentPlanningMode(value) === 'strict'
28
+ }
29
+
30
+ export function describeAgentPlanningMode(value: Agent['planningMode'] | undefined): string {
31
+ const mode = normalizeAgentPlanningMode(value)
32
+ return AGENT_PLANNING_MODE_OPTIONS.find((option) => option.value === mode)?.description
33
+ || AGENT_PLANNING_MODE_OPTIONS[0].description
34
+ }
@@ -307,7 +307,7 @@ describe('main-agent-loop', () => {
307
307
  agentId: 'agent-a',
308
308
  taskIds: [],
309
309
  currentStep: 'Verify the release checklist',
310
- plannerSummary: 'Use the mission controller instead of legacy tags.',
310
+ plannerSummary: 'Use the mission runtime instead of legacy tags.',
311
311
  verifierSummary: null,
312
312
  blockerSummary: null,
313
313
  waitState: null,
@@ -0,0 +1,112 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import {
5
+ buildRunHandoffPacket,
6
+ formatRunHandoffMarkdown,
7
+ } from './run-handoff'
8
+ import type { EvidenceArtifact, RunBrief, SessionRunRecord } from '@/types'
9
+
10
+ function run(overrides: Partial<SessionRunRecord> = {}): SessionRunRecord {
11
+ return {
12
+ id: overrides.id || 'run_1',
13
+ sessionId: overrides.sessionId || 'sess_1',
14
+ source: overrides.source || 'task',
15
+ internal: overrides.internal ?? false,
16
+ mode: overrides.mode || 'direct',
17
+ status: overrides.status || 'completed',
18
+ messagePreview: overrides.messagePreview || 'Verify the release',
19
+ queuedAt: overrides.queuedAt ?? 1000,
20
+ startedAt: overrides.startedAt ?? 1500,
21
+ endedAt: overrides.endedAt ?? 4500,
22
+ resultPreview: overrides.resultPreview || 'Release verified with browser smoke evidence.',
23
+ ownerType: overrides.ownerType ?? 'task',
24
+ ownerId: overrides.ownerId ?? 'task_1',
25
+ ...overrides,
26
+ }
27
+ }
28
+
29
+ function brief(overrides: Partial<RunBrief> = {}): RunBrief {
30
+ return {
31
+ runId: overrides.runId || 'run_1',
32
+ sessionId: overrides.sessionId || 'sess_1',
33
+ title: overrides.title || 'Verify the release',
34
+ objective: overrides.objective || 'Verify the release',
35
+ status: overrides.status || 'completed',
36
+ source: overrides.source || 'task',
37
+ owner: overrides.owner ?? { type: 'task', id: 'task_1' },
38
+ timeline: overrides.timeline || [
39
+ { label: 'Queued', status: 'queued', at: 1000 },
40
+ { label: 'Started', status: 'running', at: 1500 },
41
+ { label: 'Ended', status: 'completed', at: 4500 },
42
+ ],
43
+ result: overrides.result ?? 'Release verified with browser smoke evidence.',
44
+ error: overrides.error ?? null,
45
+ warnings: overrides.warnings || [],
46
+ usage: overrides.usage || {
47
+ inputTokens: 10,
48
+ outputTokens: 20,
49
+ estimatedCost: 0.01,
50
+ citationCount: 1,
51
+ sourceIds: ['source_1'],
52
+ },
53
+ evidence: overrides.evidence || [{
54
+ id: 'evidence_1',
55
+ kind: 'event',
56
+ title: 'Smoke test',
57
+ summary: 'Browser smoke passed.',
58
+ createdAt: 4300,
59
+ }],
60
+ generatedAt: overrides.generatedAt ?? 5000,
61
+ }
62
+ }
63
+
64
+ function artifact(overrides: Partial<EvidenceArtifact> = {}): EvidenceArtifact {
65
+ return {
66
+ id: overrides.id || 'artifact_1',
67
+ kind: overrides.kind || 'run_result',
68
+ title: overrides.title || 'Run result',
69
+ preview: overrides.preview || 'Release verified.',
70
+ createdAt: overrides.createdAt ?? 4500,
71
+ source: overrides.source || { type: 'run', id: 'run_1', label: 'Verify the release' },
72
+ ...overrides,
73
+ }
74
+ }
75
+
76
+ describe('run handoff packets', () => {
77
+ it('summarizes a completed run with evidence, artifacts, and resume commands', () => {
78
+ const packet = buildRunHandoffPacket(run(), brief(), [artifact()], 6000)
79
+
80
+ assert.equal(packet.schemaVersion, 1)
81
+ assert.equal(packet.runId, 'run_1')
82
+ assert.equal(packet.readiness.status, 'ready')
83
+ assert.equal(packet.timing.durationMs, 3000)
84
+ assert.equal(packet.evidence.length, 1)
85
+ assert.equal(packet.artifacts.length, 1)
86
+ assert.ok(packet.resume.commands.some((command) => command.includes('swarmclaw runs handoff run_1')))
87
+ assert.deepEqual(packet.readiness.recommendedActions, ['Handoff packet is ready to share.'])
88
+ })
89
+
90
+ it('marks failed and under-evidenced runs as needing attention', () => {
91
+ const packet = buildRunHandoffPacket(
92
+ run({ status: 'failed', error: 'Provider timed out.', resultPreview: undefined }),
93
+ brief({ status: 'failed', result: null, error: 'Provider timed out.', warnings: ['Run failed and needs review before using the result.'], evidence: [] }),
94
+ [],
95
+ 6000,
96
+ )
97
+
98
+ assert.equal(packet.readiness.status, 'blocked')
99
+ assert.ok(packet.readiness.recommendedActions.some((action) => action.includes('Review the run error')))
100
+ assert.ok(packet.outcome.warnings.length > 0)
101
+ })
102
+
103
+ it('formats concise markdown for handoff into another operator context', () => {
104
+ const markdown = formatRunHandoffMarkdown(buildRunHandoffPacket(run(), brief(), [artifact({ url: '/api/files/serve?path=result.md' })], 6000))
105
+
106
+ assert.match(markdown, /^# Run Handoff: Verify the release/)
107
+ assert.match(markdown, /Run ID: run_1/)
108
+ assert.match(markdown, /## Outcome/)
109
+ assert.match(markdown, /Browser smoke passed/)
110
+ assert.match(markdown, /swarmclaw chats context-pack sess_1/)
111
+ })
112
+ })
@@ -0,0 +1,171 @@
1
+ import type { EvidenceArtifact, RunBrief, RunHandoffPacket, RunHandoffReadinessStatus, SessionRunRecord } from '@/types'
2
+
3
+ const MAX_TEXT = 900
4
+ const MAX_EVIDENCE = 12
5
+ const MAX_ARTIFACTS = 16
6
+
7
+ function compactText(value: string | null | undefined, maxChars = MAX_TEXT): string | null {
8
+ const text = (value || '').split(/\s+/).filter(Boolean).join(' ').trim()
9
+ if (!text) return null
10
+ return text.length > maxChars ? `${text.slice(0, maxChars - 3)}...` : text
11
+ }
12
+
13
+ function toIso(value: number | null | undefined): string {
14
+ return value && Number.isFinite(value) ? new Date(value).toISOString() : 'n/a'
15
+ }
16
+
17
+ function durationMs(run: SessionRunRecord, now: number): number | null {
18
+ if (!run.startedAt) return null
19
+ const end = run.endedAt || now
20
+ if (!Number.isFinite(end) || end < run.startedAt) return null
21
+ return Math.max(0, Math.trunc(end - run.startedAt))
22
+ }
23
+
24
+ function readinessStatus(run: SessionRunRecord, brief: RunBrief, artifacts: EvidenceArtifact[]): RunHandoffReadinessStatus {
25
+ if (run.status === 'failed') return 'blocked'
26
+ if (run.status === 'cancelled') return 'needs_attention'
27
+ if (run.status === 'queued' || run.status === 'running') return 'needs_attention'
28
+ if (brief.warnings.length > 0) return 'needs_attention'
29
+ if (!compactText(brief.result || run.resultPreview)) return 'needs_attention'
30
+ if (brief.evidence.length === 0 && artifacts.length === 0) return 'needs_attention'
31
+ return 'ready'
32
+ }
33
+
34
+ function recommendedActions(run: SessionRunRecord, brief: RunBrief, artifacts: EvidenceArtifact[]): string[] {
35
+ const actions: string[] = []
36
+ if (run.status === 'failed') actions.push('Review the run error, fix the cause, then rerun from the source session or owner.')
37
+ if (run.status === 'cancelled') actions.push('Review why the run was cancelled before continuing the handoff.')
38
+ if (run.status === 'queued' || run.status === 'running') actions.push('Wait for the run to finish or cancel it before using the result as final.')
39
+ if (!compactText(brief.result || run.resultPreview) && run.status === 'completed') actions.push('Record a result summary before sharing this run.')
40
+ if (brief.evidence.length === 0 && artifacts.length === 0 && run.status === 'completed') {
41
+ actions.push('Attach evidence, artifacts, or a task report if another operator will continue from this run.')
42
+ }
43
+ for (const warning of brief.warnings) {
44
+ actions.push(warning)
45
+ }
46
+ return actions.length > 0 ? Array.from(new Set(actions)).slice(0, 8) : ['Handoff packet is ready to share.']
47
+ }
48
+
49
+ function resumeCommands(run: SessionRunRecord): string[] {
50
+ return [
51
+ `swarmclaw runs handoff ${run.id} --query format=markdown`,
52
+ `swarmclaw runs brief ${run.id}`,
53
+ `swarmclaw chats context-pack ${run.sessionId} --query format=markdown`,
54
+ ]
55
+ }
56
+
57
+ export function buildRunHandoffPacket(
58
+ run: SessionRunRecord,
59
+ brief: RunBrief,
60
+ artifacts: EvidenceArtifact[] = [],
61
+ now = Date.now(),
62
+ ): RunHandoffPacket {
63
+ const limitedArtifacts = artifacts.slice(0, MAX_ARTIFACTS)
64
+ return {
65
+ schemaVersion: 1,
66
+ runId: run.id,
67
+ sessionId: run.sessionId,
68
+ title: compactText(brief.title || run.messagePreview, 160) || run.id,
69
+ objective: compactText(brief.objective || run.messagePreview, 1400) || run.source,
70
+ source: run.source,
71
+ mode: run.mode,
72
+ status: run.status,
73
+ owner: brief.owner || (run.ownerType && run.ownerId ? { type: run.ownerType, id: run.ownerId } : null),
74
+ generatedAt: now,
75
+ timing: {
76
+ queuedAt: run.queuedAt,
77
+ startedAt: run.startedAt || null,
78
+ endedAt: run.endedAt || null,
79
+ durationMs: durationMs(run, now),
80
+ },
81
+ outcome: {
82
+ result: compactText(brief.result || run.resultPreview, 1400),
83
+ error: compactText(brief.error || run.error, 1400),
84
+ warnings: brief.warnings.slice(0, 12),
85
+ },
86
+ usage: brief.usage,
87
+ timeline: brief.timeline.slice(0, 20),
88
+ evidence: brief.evidence.slice(0, MAX_EVIDENCE),
89
+ artifacts: limitedArtifacts,
90
+ resume: {
91
+ sessionId: run.sessionId,
92
+ commands: resumeCommands(run),
93
+ links: [
94
+ { label: 'Run events', href: `/api/runs/${encodeURIComponent(run.id)}/events` },
95
+ { label: 'Run brief', href: `/api/runs/${encodeURIComponent(run.id)}/brief` },
96
+ { label: 'Session context pack', href: `/api/chats/${encodeURIComponent(run.sessionId)}/context-pack?format=markdown` },
97
+ ],
98
+ },
99
+ readiness: {
100
+ status: readinessStatus(run, brief, limitedArtifacts),
101
+ recommendedActions: recommendedActions(run, brief, limitedArtifacts),
102
+ },
103
+ }
104
+ }
105
+
106
+ function appendSection(lines: string[], title: string, body: string[] = []) {
107
+ lines.push('', `## ${title}`)
108
+ if (body.length === 0) lines.push('None.')
109
+ else lines.push(...body)
110
+ }
111
+
112
+ function artifactLine(artifact: EvidenceArtifact): string {
113
+ const target = artifact.url || artifact.href || ''
114
+ const preview = compactText(artifact.preview || artifact.description, 280)
115
+ return `- ${artifact.title} (${artifact.kind})${target ? ` ${target}` : ''}${preview ? `: ${preview}` : ''}`
116
+ }
117
+
118
+ export function formatRunHandoffMarkdown(packet: RunHandoffPacket): string {
119
+ const owner = packet.owner ? `${packet.owner.type}:${packet.owner.id}` : 'unassigned'
120
+ const duration = packet.timing.durationMs == null ? 'n/a' : `${Math.round(packet.timing.durationMs / 1000)}s`
121
+ const lines = [
122
+ `# Run Handoff: ${packet.title}`,
123
+ '',
124
+ `Generated: ${toIso(packet.generatedAt)}`,
125
+ `Run ID: ${packet.runId}`,
126
+ `Session ID: ${packet.sessionId}`,
127
+ `Status: ${packet.status}`,
128
+ `Readiness: ${packet.readiness.status}`,
129
+ `Source: ${packet.source}`,
130
+ `Owner: ${owner}`,
131
+ `Duration: ${duration}`,
132
+ ]
133
+
134
+ appendSection(lines, 'Objective', [packet.objective])
135
+
136
+ appendSection(lines, 'Outcome', [
137
+ packet.outcome.result ? `- Result: ${packet.outcome.result}` : '',
138
+ packet.outcome.error ? `- Error: ${packet.outcome.error}` : '',
139
+ ...packet.outcome.warnings.map((warning) => `- Warning: ${warning}`),
140
+ ].filter(Boolean))
141
+
142
+ appendSection(lines, 'Timeline', packet.timeline.map((item) => {
143
+ const status = item.status ? ` (${item.status})` : ''
144
+ const detail = item.detail ? `: ${compactText(item.detail, 260)}` : ''
145
+ return `- ${item.label}${status} at ${toIso(item.at)}${detail}`
146
+ }))
147
+
148
+ appendSection(lines, 'Evidence', packet.evidence.map((item) => {
149
+ const url = item.url ? ` ${item.url}` : ''
150
+ return `- ${item.title} (${item.kind})${url}: ${item.summary}`
151
+ }))
152
+
153
+ appendSection(lines, 'Artifacts', packet.artifacts.map(artifactLine))
154
+
155
+ appendSection(lines, 'Usage', [
156
+ `- Input tokens: ${packet.usage.inputTokens ?? 0}`,
157
+ `- Output tokens: ${packet.usage.outputTokens ?? 0}`,
158
+ packet.usage.estimatedCost != null ? `- Estimated cost: $${packet.usage.estimatedCost.toFixed(4)}` : '',
159
+ `- Citations: ${packet.usage.citationCount}`,
160
+ packet.usage.sourceIds.length > 0 ? `- Sources: ${packet.usage.sourceIds.join(', ')}` : '',
161
+ ].filter(Boolean))
162
+
163
+ appendSection(lines, 'Resume', [
164
+ ...packet.resume.commands.map((command) => `- \`${command}\``),
165
+ ...packet.resume.links.map((link) => `- ${link.label}: ${link.href}`),
166
+ ])
167
+
168
+ appendSection(lines, 'Recommended Actions', packet.readiness.recommendedActions.map((action) => `- ${action}`))
169
+
170
+ return `${lines.join('\n')}\n`
171
+ }
@@ -301,6 +301,7 @@ export interface AgentPackEntry {
301
301
  extensions?: string[]
302
302
  capabilities?: string[]
303
303
  elevenLabsVoiceId?: string | null
304
+ planningMode?: 'off' | 'strict' | null
304
305
  soul?: string
305
306
  systemPrompt?: string
306
307
  }
@@ -17,6 +17,7 @@ export * from './goal'
17
17
  export * from './mission'
18
18
  export * from './operations'
19
19
  export * from './run-brief'
20
+ export * from './run-handoff'
20
21
  export * from './artifact'
21
22
  export * from './swarmdock'
22
23
  export * from './dream'
@@ -0,0 +1,48 @@
1
+ import type { EvidenceArtifact } from './artifact'
2
+ import type { ExecutionOwnerType, SessionRunStatus } from './run'
3
+ import type { RunBriefEvidenceItem, RunBriefTimelineItem } from './run-brief'
4
+
5
+ export type RunHandoffReadinessStatus = 'ready' | 'needs_attention' | 'blocked'
6
+
7
+ export interface RunHandoffPacket {
8
+ schemaVersion: 1
9
+ runId: string
10
+ sessionId: string
11
+ title: string
12
+ objective: string
13
+ source: string
14
+ mode: string
15
+ status: SessionRunStatus
16
+ owner: { type: ExecutionOwnerType; id: string } | null
17
+ generatedAt: number
18
+ timing: {
19
+ queuedAt: number
20
+ startedAt: number | null
21
+ endedAt: number | null
22
+ durationMs: number | null
23
+ }
24
+ outcome: {
25
+ result: string | null
26
+ error: string | null
27
+ warnings: string[]
28
+ }
29
+ usage: {
30
+ inputTokens: number | null
31
+ outputTokens: number | null
32
+ estimatedCost: number | null
33
+ citationCount: number
34
+ sourceIds: string[]
35
+ }
36
+ timeline: RunBriefTimelineItem[]
37
+ evidence: RunBriefEvidenceItem[]
38
+ artifacts: EvidenceArtifact[]
39
+ resume: {
40
+ sessionId: string
41
+ commands: string[]
42
+ links: Array<{ label: string; href: string }>
43
+ }
44
+ readiness: {
45
+ status: RunHandoffReadinessStatus
46
+ recommendedActions: string[]
47
+ }
48
+ }