@swarmclawai/swarmclaw 1.9.13 → 1.9.15
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 +18 -0
- package/package.json +2 -2
- package/src/app/api/chats/[id]/context-pack/route.ts +43 -0
- package/src/app/api/chats/context-pack-route.test.ts +109 -0
- package/src/app/api/runs/[id]/handoff/route.ts +26 -0
- package/src/app/api/runs/run-handoff-route.test.ts +120 -0
- package/src/cli/index.js +2 -0
- package/src/cli/spec.js +2 -0
- package/src/components/chat/chat-header.tsx +36 -3
- package/src/components/runs/run-list.tsx +44 -6
- package/src/lib/server/agents/main-agent-loop.test.ts +1 -1
- package/src/lib/server/chats/session-context-pack.test.ts +121 -0
- package/src/lib/server/chats/session-context-pack.ts +387 -0
- package/src/lib/server/memory/memory-abstract.ts +0 -1
- package/src/lib/server/memory/temporal-decay.ts +0 -1
- package/src/lib/server/runs/run-handoff.test.ts +112 -0
- package/src/lib/server/runs/run-handoff.ts +171 -0
- package/src/lib/server/runtime/wake-mode.ts +0 -3
- package/src/lib/server/skills/skill-prompt-budget.ts +2 -2
- package/src/lib/server/workspace-context.ts +0 -3
- package/src/types/index.ts +1 -0
- package/src/types/run-handoff.ts +48 -0
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.15 Highlights
|
|
403
|
+
|
|
404
|
+
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.
|
|
405
|
+
|
|
406
|
+
- **Run handoff API.** `GET /api/runs/:id/handoff` returns structured handoff JSON, and `?format=markdown` returns copyable markdown.
|
|
407
|
+
- **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.
|
|
408
|
+
- **CLI access.** `swarmclaw runs handoff <runId> --query format=markdown` exposes the same packet for scripts and release automation.
|
|
409
|
+
- **Readiness guidance.** Packets mark failed, cancelled, running, warning, or under-evidenced runs as blocked or needing attention before another operator relies on the result.
|
|
410
|
+
|
|
411
|
+
### v1.9.14 Highlights
|
|
412
|
+
|
|
413
|
+
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.
|
|
414
|
+
|
|
415
|
+
- **Context-pack API.** `GET /api/chats/:id/context-pack` returns structured handoff JSON, and `?format=markdown` returns copyable markdown.
|
|
416
|
+
- **Chat header copy action.** Active chats with messages expose a context-pack button for quick handoff to another operator or backend.
|
|
417
|
+
- **CLI access.** `swarmclaw chats context-pack <chatId> --query format=markdown` exposes the same packet for scripts and release automation.
|
|
418
|
+
- **Smoke coverage.** Runtime tests and the browser smoke gate now verify the context-pack route and markdown response.
|
|
419
|
+
|
|
402
420
|
### v1.9.13 Highlights
|
|
403
421
|
|
|
404
422
|
Architecture health release: SwarmClaw now turns runtime ownership, dispatch, memory, startup, and quality evidence into a scored operator report.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.15",
|
|
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/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-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/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/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",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
|
|
3
|
+
import { buildSessionContextPack, formatSessionContextPackMarkdown } from '@/lib/server/chats/session-context-pack'
|
|
4
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
|
+
import { getMessages } from '@/lib/server/messages/message-repository'
|
|
6
|
+
import { getSession } from '@/lib/server/sessions/session-repository'
|
|
7
|
+
import { listTasks } from '@/lib/server/tasks/task-repository'
|
|
8
|
+
|
|
9
|
+
const DEFAULT_RECENT_MESSAGES = 12
|
|
10
|
+
const MAX_RECENT_MESSAGES = 40
|
|
11
|
+
|
|
12
|
+
function parseMaxRecentMessages(req: Request): number {
|
|
13
|
+
const url = new URL(req.url)
|
|
14
|
+
const raw = url.searchParams.get('messages') || url.searchParams.get('maxRecentMessages')
|
|
15
|
+
const parsed = Number.parseInt(raw || '', 10)
|
|
16
|
+
if (!Number.isFinite(parsed)) return DEFAULT_RECENT_MESSAGES
|
|
17
|
+
return Math.max(1, Math.min(MAX_RECENT_MESSAGES, parsed))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
21
|
+
const { id } = await params
|
|
22
|
+
const session = getSession(id)
|
|
23
|
+
if (!session) return notFound()
|
|
24
|
+
|
|
25
|
+
const url = new URL(req.url)
|
|
26
|
+
const format = url.searchParams.get('format')
|
|
27
|
+
const pack = buildSessionContextPack({
|
|
28
|
+
session,
|
|
29
|
+
messages: getMessages(id),
|
|
30
|
+
tasks: listTasks(),
|
|
31
|
+
maxRecentMessages: parseMaxRecentMessages(req),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
if (format === 'markdown') {
|
|
35
|
+
return new Response(formatSessionContextPackMarkdown(pack), {
|
|
36
|
+
headers: {
|
|
37
|
+
'content-type': 'text/markdown; charset=utf-8',
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return NextResponse.json(pack)
|
|
43
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
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/chats/[id]/context-pack returns structured and markdown handoff context', () => {
|
|
7
|
+
const output = runWithTempDataDir<{
|
|
8
|
+
status: number
|
|
9
|
+
markdownStatus: number
|
|
10
|
+
missingStatus: number
|
|
11
|
+
schemaVersion: number
|
|
12
|
+
linkedTaskCount: number
|
|
13
|
+
recentMessageCount: number
|
|
14
|
+
markdownContentType: string
|
|
15
|
+
markdownIncludesTitle: boolean
|
|
16
|
+
markdownIncludesTask: boolean
|
|
17
|
+
}>(`
|
|
18
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
19
|
+
const repoMod = await import('@/lib/server/messages/message-repository')
|
|
20
|
+
const routeMod = await import('./src/app/api/chats/[id]/context-pack/route')
|
|
21
|
+
const storage = storageMod.default || storageMod
|
|
22
|
+
const repo = repoMod.default || repoMod
|
|
23
|
+
const route = routeMod.default || routeMod
|
|
24
|
+
|
|
25
|
+
const now = Date.now()
|
|
26
|
+
storage.saveSessions({
|
|
27
|
+
sess_pack_1: {
|
|
28
|
+
id: 'sess_pack_1',
|
|
29
|
+
name: 'Context pack test',
|
|
30
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
31
|
+
user: 'tester',
|
|
32
|
+
provider: 'openai',
|
|
33
|
+
model: 'gpt-4o-mini',
|
|
34
|
+
claudeSessionId: null,
|
|
35
|
+
codexThreadId: 'codex-pack-thread',
|
|
36
|
+
messages: [],
|
|
37
|
+
createdAt: now,
|
|
38
|
+
lastActiveAt: now,
|
|
39
|
+
runContext: {
|
|
40
|
+
objective: 'Hand off a release session.',
|
|
41
|
+
constraints: ['Keep the pack short.'],
|
|
42
|
+
keyFacts: ['The current tag is local only.'],
|
|
43
|
+
discoveries: [],
|
|
44
|
+
failedApproaches: [],
|
|
45
|
+
currentPlan: ['Run checks'],
|
|
46
|
+
completedSteps: [],
|
|
47
|
+
blockers: [],
|
|
48
|
+
parentContext: null,
|
|
49
|
+
updatedAt: now,
|
|
50
|
+
version: 1,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
storage.saveTasks({
|
|
55
|
+
task_pack_1: {
|
|
56
|
+
id: 'task_pack_1',
|
|
57
|
+
title: 'Verify context pack',
|
|
58
|
+
description: 'Exercise API route.',
|
|
59
|
+
status: 'running',
|
|
60
|
+
sessionId: 'sess_pack_1',
|
|
61
|
+
agentId: null,
|
|
62
|
+
createdAt: now,
|
|
63
|
+
updatedAt: now + 3,
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
repo.appendMessage('sess_pack_1', { role: 'user', text: 'Prepare handoff.', time: now, attachedFiles: ['/tmp/handoff.md'] })
|
|
68
|
+
repo.appendMessage('sess_pack_1', { role: 'assistant', text: 'Ready.', time: now + 1 })
|
|
69
|
+
|
|
70
|
+
const response = await route.GET(
|
|
71
|
+
new Request('http://local/api/chats/sess_pack_1/context-pack?messages=1'),
|
|
72
|
+
{ params: Promise.resolve({ id: 'sess_pack_1' }) },
|
|
73
|
+
)
|
|
74
|
+
const payload = await response.json()
|
|
75
|
+
|
|
76
|
+
const markdownResponse = await route.GET(
|
|
77
|
+
new Request('http://local/api/chats/sess_pack_1/context-pack?format=markdown'),
|
|
78
|
+
{ params: Promise.resolve({ id: 'sess_pack_1' }) },
|
|
79
|
+
)
|
|
80
|
+
const markdown = await markdownResponse.text()
|
|
81
|
+
|
|
82
|
+
const missingResponse = await route.GET(
|
|
83
|
+
new Request('http://local/api/chats/missing/context-pack'),
|
|
84
|
+
{ params: Promise.resolve({ id: 'missing' }) },
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
console.log(JSON.stringify({
|
|
88
|
+
status: response.status,
|
|
89
|
+
markdownStatus: markdownResponse.status,
|
|
90
|
+
missingStatus: missingResponse.status,
|
|
91
|
+
schemaVersion: payload.schemaVersion,
|
|
92
|
+
linkedTaskCount: payload.linkedTasks.length,
|
|
93
|
+
recentMessageCount: payload.recentMessages.length,
|
|
94
|
+
markdownContentType: markdownResponse.headers.get('content-type') || '',
|
|
95
|
+
markdownIncludesTitle: markdown.includes('# Session Context Pack: Context pack test'),
|
|
96
|
+
markdownIncludesTask: markdown.includes('Verify context pack'),
|
|
97
|
+
}))
|
|
98
|
+
`, { prefix: 'swarmclaw-context-pack-route-' })
|
|
99
|
+
|
|
100
|
+
assert.equal(output.status, 200)
|
|
101
|
+
assert.equal(output.markdownStatus, 200)
|
|
102
|
+
assert.equal(output.missingStatus, 404)
|
|
103
|
+
assert.equal(output.schemaVersion, 1)
|
|
104
|
+
assert.equal(output.linkedTaskCount, 1)
|
|
105
|
+
assert.equal(output.recentMessageCount, 1)
|
|
106
|
+
assert.match(output.markdownContentType, /text\/markdown/)
|
|
107
|
+
assert.equal(output.markdownIncludesTitle, true)
|
|
108
|
+
assert.equal(output.markdownIncludesTask, true)
|
|
109
|
+
})
|
|
@@ -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
|
{
|
|
@@ -627,6 +628,7 @@ const COMMAND_GROUPS = [
|
|
|
627
628
|
cmd('clear-undo', 'POST', '/chats/:id/clear/undo', 'Restore a cleared chat via its undoToken', { expectsJsonBody: true }),
|
|
628
629
|
cmd('compact', 'POST', '/chats/:id/compact', 'Summarize and compact chat history (accepts optional keepLastN)', { expectsJsonBody: true }),
|
|
629
630
|
cmd('context-status', 'GET', '/chats/:id/context-status', 'Report token usage and context-window status for a chat'),
|
|
631
|
+
cmd('context-pack', 'GET', '/chats/:id/context-pack', 'Get a copyable session context pack for handoff'),
|
|
630
632
|
cmd('browser-status', 'GET', '/chats/:id/browser', 'Check browser status'),
|
|
631
633
|
cmd('browser-close', 'DELETE', '/chats/:id/browser', 'Close browser'),
|
|
632
634
|
cmd('mailbox', 'GET', '/chats/:id/mailbox', 'List chat mailbox envelopes'),
|
package/src/cli/spec.js
CHANGED
|
@@ -458,6 +458,7 @@ const COMMAND_GROUPS = {
|
|
|
458
458
|
'clear-undo': { description: 'Restore a cleared chat via its undoToken', method: 'POST', path: '/chats/:id/clear/undo', params: ['id'] },
|
|
459
459
|
compact: { description: 'Summarize and compact chat history', method: 'POST', path: '/chats/:id/compact', params: ['id'] },
|
|
460
460
|
'context-status': { description: 'Report token usage and context-window status', method: 'GET', path: '/chats/:id/context-status', params: ['id'] },
|
|
461
|
+
'context-pack': { description: 'Get a copyable session context pack for handoff', method: 'GET', path: '/chats/:id/context-pack', params: ['id'] },
|
|
461
462
|
mailbox: { description: 'List mailbox envelopes for a chat', method: 'GET', path: '/chats/:id/mailbox', params: ['id'] },
|
|
462
463
|
'mailbox-action': { description: 'Send/ack/clear mailbox envelopes', method: 'POST', path: '/chats/:id/mailbox', params: ['id'] },
|
|
463
464
|
queue: { description: 'List queued follow-up turns for a chat', method: 'GET', path: '/chats/:id/queue', params: ['id'] },
|
|
@@ -551,6 +552,7 @@ const COMMAND_GROUPS = {
|
|
|
551
552
|
get: { description: 'Get run by id', method: 'GET', path: '/runs/:id', params: ['id'] },
|
|
552
553
|
events: { description: 'Get run event history by run id', method: 'GET', path: '/runs/:id/events', params: ['id'] },
|
|
553
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'] },
|
|
554
556
|
},
|
|
555
557
|
},
|
|
556
558
|
webhooks: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState, useMemo, useRef, type ReactNode } from 'react'
|
|
4
|
-
import { Plus } from 'lucide-react'
|
|
4
|
+
import { ClipboardList, Plus } from 'lucide-react'
|
|
5
5
|
import type { Session } from '@/types'
|
|
6
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
7
7
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
@@ -108,6 +108,8 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
108
108
|
const connectorMeta = connector ? resolveConnectorPlatformMeta(connector.platform) : null
|
|
109
109
|
const connectorPresence = connector?.presence
|
|
110
110
|
const [copied, setCopied] = useState(false)
|
|
111
|
+
const [contextPackCopied, setContextPackCopied] = useState(false)
|
|
112
|
+
const [contextPackLoading, setContextPackLoading] = useState(false)
|
|
111
113
|
const [sourceDropdownOpen, setSourceDropdownOpen] = useState(false)
|
|
112
114
|
const sourceDropdownRef = useRef<HTMLDivElement>(null)
|
|
113
115
|
const [renaming, setRenaming] = useState(false)
|
|
@@ -220,6 +222,22 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
220
222
|
})
|
|
221
223
|
}
|
|
222
224
|
|
|
225
|
+
const handleCopyContextPack = async () => {
|
|
226
|
+
if (contextPackLoading) return
|
|
227
|
+
setContextPackLoading(true)
|
|
228
|
+
try {
|
|
229
|
+
const markdown = await api<string>('GET', `/chats/${session.id}/context-pack?format=markdown`)
|
|
230
|
+
const copiedPack = await copyTextToClipboard(markdown)
|
|
231
|
+
if (!copiedPack) return
|
|
232
|
+
setContextPackCopied(true)
|
|
233
|
+
setTimeout(() => setContextPackCopied(false), 2000)
|
|
234
|
+
} catch {
|
|
235
|
+
// Best-effort clipboard helper; the route remains available from the CLI.
|
|
236
|
+
} finally {
|
|
237
|
+
setContextPackLoading(false)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
223
241
|
const handleDismissResumeHandle = async (e: React.MouseEvent) => {
|
|
224
242
|
e.stopPropagation()
|
|
225
243
|
try {
|
|
@@ -302,10 +320,11 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
302
320
|
}
|
|
303
321
|
}, [session.name, loadConnectors])
|
|
304
322
|
|
|
305
|
-
// Context bar shows for memories, source filter, task links, resume handles, browser
|
|
323
|
+
// Context bar shows for handoff packs, memories, source filter, task links, resume handles, browser
|
|
324
|
+
const hasContextPack = messageCount > 0
|
|
306
325
|
const hasMemoryLink = !!(agent && getEnabledToolIds(session).includes('memory'))
|
|
307
326
|
const hasSourceFilter = !!hasMultipleSources
|
|
308
|
-
const hasContextBar = !!(hasMemoryLink || hasSourceFilter || linkedTask || resumeHandle || browserActive)
|
|
327
|
+
const hasContextBar = !!(hasContextPack || hasMemoryLink || hasSourceFilter || linkedTask || resumeHandle || browserActive)
|
|
309
328
|
|
|
310
329
|
return (
|
|
311
330
|
<>
|
|
@@ -523,6 +542,20 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
523
542
|
{hasContextBar && (
|
|
524
543
|
<div className="border-t border-white/[0.05] bg-black/[0.08] px-4 py-2">
|
|
525
544
|
<div className="flex items-center gap-1.5 flex-wrap">
|
|
545
|
+
{hasContextPack && (
|
|
546
|
+
<Tip label="Copy session context pack">
|
|
547
|
+
<button
|
|
548
|
+
type="button"
|
|
549
|
+
onClick={handleCopyContextPack}
|
|
550
|
+
disabled={contextPackLoading}
|
|
551
|
+
aria-label="Copy session context pack"
|
|
552
|
+
className="flex min-w-[122px] items-center justify-center gap-1 px-2.5 py-1 rounded-[8px] bg-white/[0.03] hover:bg-white/[0.06] disabled:opacity-70 disabled:cursor-wait transition-colors cursor-pointer text-[10px] font-600 text-text-3/60 hover:text-text-2 shrink-0"
|
|
553
|
+
>
|
|
554
|
+
<ClipboardList className="h-3 w-3 shrink-0" aria-hidden="true" strokeWidth={2.2} />
|
|
555
|
+
<span>{contextPackLoading ? 'Packing' : contextPackCopied ? 'Copied' : 'Context pack'}</span>
|
|
556
|
+
</button>
|
|
557
|
+
</Tip>
|
|
558
|
+
)}
|
|
526
559
|
{hasMemoryLink && (
|
|
527
560
|
<Tip label="View agent memories">
|
|
528
561
|
<button
|
|
@@ -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
|
|
252
|
-
<
|
|
253
|
-
{selected.status}
|
|
254
|
-
|
|
255
|
-
|
|
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>
|
|
@@ -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
|
|
310
|
+
plannerSummary: 'Use the mission runtime instead of legacy tags.',
|
|
311
311
|
verifierSummary: null,
|
|
312
312
|
blockerSummary: null,
|
|
313
313
|
waitState: null,
|