@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
|
@@ -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
|
+
}
|
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Replaces the implicit `source: 'heartbeat' | 'heartbeat-wake'` convention
|
|
5
5
|
* with a formal enum that determines routing, priority, and isolation behavior.
|
|
6
|
-
*
|
|
7
|
-
* Inspired by OpenClaw's separation of "run now" vs "queue next heartbeat" vs
|
|
8
|
-
* scheduled execution with proper isolation.
|
|
9
6
|
*/
|
|
10
7
|
|
|
11
8
|
// ── WakeMode enum ───────────────────────────────────────────────────────
|
|
@@ -84,8 +84,8 @@ export function budgetSkillsForPrompt(
|
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
86
|
* Prescriptive skill adherence header.
|
|
87
|
-
* This tells the model exactly when and how to use skills
|
|
88
|
-
*
|
|
87
|
+
* This tells the model exactly when and how to use skills so it can keep
|
|
88
|
+
* skill-backed turns focused and economical.
|
|
89
89
|
*/
|
|
90
90
|
const SKILL_ADHERENCE_HEADER = `## Skills
|
|
91
91
|
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Workspace context injection — injects workspace files into the agent's system prompt.
|
|
3
3
|
*
|
|
4
|
-
* Inspired by OpenClaw's pattern of injecting HEARTBEAT.md, IDENTITY.md, AGENTS.md,
|
|
5
|
-
* SOUL.md, TOOLS.md, USER.md, and BOOTSTRAP.md into every agent turn.
|
|
6
|
-
*
|
|
7
4
|
* This gives agents self-awareness, goals, and context about their operating environment
|
|
8
5
|
* without requiring the user to manually configure everything.
|
|
9
6
|
*/
|
package/src/types/index.ts
CHANGED
|
@@ -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
|
+
}
|