@agentsquared/cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +420 -0
- package/a2_cli.mjs +1576 -0
- package/adapters/index.mjs +79 -0
- package/adapters/openclaw/adapter.mjs +1020 -0
- package/adapters/openclaw/cli.mjs +89 -0
- package/adapters/openclaw/detect.mjs +259 -0
- package/adapters/openclaw/helpers.mjs +827 -0
- package/adapters/openclaw/ws_client.mjs +740 -0
- package/bin/a2-cli.js +8 -0
- package/lib/conversation/policy.mjs +122 -0
- package/lib/conversation/store.mjs +223 -0
- package/lib/conversation/templates.mjs +419 -0
- package/lib/gateway/api.mjs +28 -0
- package/lib/gateway/inbox.mjs +344 -0
- package/lib/gateway/lifecycle.mjs +602 -0
- package/lib/gateway/runtime_state.mjs +388 -0
- package/lib/gateway/server.mjs +883 -0
- package/lib/gateway/state.mjs +175 -0
- package/lib/routing/agent_router.mjs +511 -0
- package/lib/runtime/executor.mjs +380 -0
- package/lib/runtime/keys.mjs +85 -0
- package/lib/runtime/report.mjs +302 -0
- package/lib/runtime/safety.mjs +72 -0
- package/lib/shared/paths.mjs +155 -0
- package/lib/shared/primitives.mjs +43 -0
- package/lib/transport/http_json.mjs +96 -0
- package/lib/transport/libp2p.mjs +397 -0
- package/lib/transport/peer_session.mjs +857 -0
- package/lib/transport/relay_http.mjs +110 -0
- package/package.json +53 -0
package/bin/a2-cli.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
function clean(value) {
|
|
2
|
+
return `${value ?? ''}`.trim()
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export const PLATFORM_MAX_TURNS = 20
|
|
6
|
+
|
|
7
|
+
const DEFAULT_SKILL_MAX_TURNS = Object.freeze({
|
|
8
|
+
'friend-im': 1,
|
|
9
|
+
'agent-mutual-learning': 8
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const VALID_DECISIONS = new Set(['continue', 'done', 'handoff'])
|
|
13
|
+
|
|
14
|
+
const VALID_STOP_REASONS = new Set([
|
|
15
|
+
'goal-satisfied',
|
|
16
|
+
'no-new-information',
|
|
17
|
+
'receiver-budget-limit',
|
|
18
|
+
'safety-block',
|
|
19
|
+
'owner-approval-required',
|
|
20
|
+
'unsafe-or-sensitive',
|
|
21
|
+
'max-turns-reached',
|
|
22
|
+
'peer-requested-stop',
|
|
23
|
+
'timeout',
|
|
24
|
+
'single-turn',
|
|
25
|
+
'receiver-runtime-unavailable'
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
function parsePositiveInteger(value, fallback = 0) {
|
|
29
|
+
const parsed = Number.parseInt(`${value ?? ''}`, 10)
|
|
30
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
31
|
+
return fallback
|
|
32
|
+
}
|
|
33
|
+
return parsed
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function extractFrontmatter(text = '') {
|
|
37
|
+
const match = `${text ?? ''}`.match(/^---\n([\s\S]*?)\n---/)
|
|
38
|
+
return match?.[1] ?? ''
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readFrontmatterValue(frontmatter = '', key = '') {
|
|
42
|
+
const match = `${frontmatter}`.match(new RegExp(`^\\s*${clean(key)}\\s*:\\s*(.+)\\s*$`, 'm'))
|
|
43
|
+
return clean(match?.[1] ?? '').replace(/^["']|["']$/g, '')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function clampConversationMaxTurns(value, fallback = 1) {
|
|
47
|
+
const boundedFallback = Math.max(1, Math.min(PLATFORM_MAX_TURNS, parsePositiveInteger(fallback, 1) || 1))
|
|
48
|
+
const parsed = parsePositiveInteger(value, boundedFallback)
|
|
49
|
+
return Math.max(1, Math.min(PLATFORM_MAX_TURNS, parsed || boundedFallback))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function parseSkillDocumentPolicy(text = '', {
|
|
53
|
+
fallbackName = ''
|
|
54
|
+
} = {}) {
|
|
55
|
+
const frontmatter = extractFrontmatter(text)
|
|
56
|
+
const name = clean(readFrontmatterValue(frontmatter, 'name')) || clean(fallbackName)
|
|
57
|
+
const maxTurns = clampConversationMaxTurns(
|
|
58
|
+
readFrontmatterValue(frontmatter, 'maxTurns') || readFrontmatterValue(frontmatter, 'max_turns'),
|
|
59
|
+
DEFAULT_SKILL_MAX_TURNS[name] ?? 1
|
|
60
|
+
)
|
|
61
|
+
return {
|
|
62
|
+
name,
|
|
63
|
+
maxTurns
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function resolveSkillMaxTurns(skillName = '', sharedSkill = null) {
|
|
68
|
+
const normalizedSkill = clean(skillName).toLowerCase()
|
|
69
|
+
const sharedSkillName = clean(sharedSkill?.name).toLowerCase()
|
|
70
|
+
if (sharedSkillName && normalizedSkill && sharedSkillName === normalizedSkill) {
|
|
71
|
+
return clampConversationMaxTurns(sharedSkill?.maxTurns, DEFAULT_SKILL_MAX_TURNS[normalizedSkill] ?? 1)
|
|
72
|
+
}
|
|
73
|
+
if (normalizedSkill in DEFAULT_SKILL_MAX_TURNS) {
|
|
74
|
+
return clampConversationMaxTurns(DEFAULT_SKILL_MAX_TURNS[normalizedSkill], 1)
|
|
75
|
+
}
|
|
76
|
+
return 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function normalizeConversationControl(raw = {}, {
|
|
80
|
+
defaultTurnIndex = 1,
|
|
81
|
+
defaultDecision = 'done',
|
|
82
|
+
defaultStopReason = '',
|
|
83
|
+
defaultFinalize = null
|
|
84
|
+
} = {}) {
|
|
85
|
+
const turnIndex = Math.max(1, parsePositiveInteger(raw?.turnIndex, defaultTurnIndex || 1) || 1)
|
|
86
|
+
const decision = VALID_DECISIONS.has(clean(raw?.decision).toLowerCase())
|
|
87
|
+
? clean(raw?.decision).toLowerCase()
|
|
88
|
+
: clean(defaultDecision).toLowerCase() || 'done'
|
|
89
|
+
const stopReason = VALID_STOP_REASONS.has(clean(raw?.stopReason).toLowerCase())
|
|
90
|
+
? clean(raw?.stopReason).toLowerCase()
|
|
91
|
+
: clean(defaultStopReason).toLowerCase()
|
|
92
|
+
const computedFinalize = typeof defaultFinalize === 'boolean'
|
|
93
|
+
? defaultFinalize
|
|
94
|
+
: decision !== 'continue'
|
|
95
|
+
const finalize = typeof raw?.finalize === 'boolean'
|
|
96
|
+
? raw.finalize
|
|
97
|
+
: computedFinalize
|
|
98
|
+
return {
|
|
99
|
+
turnIndex,
|
|
100
|
+
decision,
|
|
101
|
+
stopReason,
|
|
102
|
+
finalize: finalize || decision !== 'continue'
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function shouldContinueConversation(control = {}) {
|
|
107
|
+
const normalized = normalizeConversationControl(control)
|
|
108
|
+
return normalized.decision === 'continue' && !normalized.finalize
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function resolveInboundConversationIdentity(item = {}) {
|
|
112
|
+
const metadata = item?.request?.params?.metadata
|
|
113
|
+
const normalizedMetadata = metadata && typeof metadata === 'object' ? metadata : {}
|
|
114
|
+
const explicitConversationKey = clean(normalizedMetadata.conversationKey)
|
|
115
|
+
if (explicitConversationKey) {
|
|
116
|
+
return {
|
|
117
|
+
conversationKey: explicitConversationKey,
|
|
118
|
+
mailboxKey: `conversation:${explicitConversationKey}`
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
throw Object.assign(new Error('conversationKey is required for inbound AgentSquared conversations'), { code: 400 })
|
|
122
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
function clean(value) {
|
|
4
|
+
return `${value ?? ''}`.trim()
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function clone(value) {
|
|
8
|
+
return JSON.parse(JSON.stringify(value))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function nowISO() {
|
|
12
|
+
return new Date().toISOString()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function stableTurnFingerprint({
|
|
16
|
+
turnIndex,
|
|
17
|
+
remoteAgentId,
|
|
18
|
+
inboundText,
|
|
19
|
+
replyText,
|
|
20
|
+
selectedSkill
|
|
21
|
+
} = {}) {
|
|
22
|
+
const payload = JSON.stringify({
|
|
23
|
+
turnIndex: Number.parseInt(`${turnIndex ?? 1}`, 10) || 1,
|
|
24
|
+
remoteAgentId: clean(remoteAgentId).toLowerCase(),
|
|
25
|
+
inboundText: clean(inboundText).replace(/\s+/g, ' ').trim(),
|
|
26
|
+
replyText: clean(replyText).replace(/\s+/g, ' ').trim(),
|
|
27
|
+
selectedSkill: clean(selectedSkill)
|
|
28
|
+
})
|
|
29
|
+
return crypto.createHash('sha256').update(payload).digest('hex')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function excerpt(text, maxLength = 240) {
|
|
33
|
+
const compact = clean(text).replace(/\s+/g, ' ').trim()
|
|
34
|
+
if (!compact) {
|
|
35
|
+
return ''
|
|
36
|
+
}
|
|
37
|
+
return compact.length > maxLength ? `${compact.slice(0, maxLength - 3)}...` : compact
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatTranscript(turns = []) {
|
|
41
|
+
if (!Array.isArray(turns) || turns.length === 0) {
|
|
42
|
+
return ''
|
|
43
|
+
}
|
|
44
|
+
return turns.map((turn) => {
|
|
45
|
+
const lines = [
|
|
46
|
+
`Turn ${turn.turnIndex}:`,
|
|
47
|
+
`- Remote message: ${excerpt(turn.inboundText, 400) || '(empty)'}`,
|
|
48
|
+
`- My reply: ${excerpt(turn.replyText, 400) || '(empty)'}`,
|
|
49
|
+
`- Decision: ${clean(turn.decision) || 'done'}`
|
|
50
|
+
]
|
|
51
|
+
if (clean(turn.stopReason)) {
|
|
52
|
+
lines.push(`- Stop reason: ${clean(turn.stopReason)}`)
|
|
53
|
+
}
|
|
54
|
+
return lines.join('\n')
|
|
55
|
+
}).join('\n\n')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createLiveConversationStore() {
|
|
59
|
+
const conversations = new Map()
|
|
60
|
+
|
|
61
|
+
function resolveConversationKey({
|
|
62
|
+
conversationKey,
|
|
63
|
+
peerSessionId
|
|
64
|
+
} = {}) {
|
|
65
|
+
return clean(conversationKey) || clean(peerSessionId)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function ensureConversation({
|
|
69
|
+
conversationKey,
|
|
70
|
+
peerSessionId,
|
|
71
|
+
remoteAgentId,
|
|
72
|
+
selectedSkill
|
|
73
|
+
} = {}) {
|
|
74
|
+
const key = resolveConversationKey({ conversationKey, peerSessionId })
|
|
75
|
+
if (!key) {
|
|
76
|
+
throw new Error('conversationKey or peerSessionId is required for live conversation state')
|
|
77
|
+
}
|
|
78
|
+
const existing = conversations.get(key)
|
|
79
|
+
if (existing) {
|
|
80
|
+
return clone(existing)
|
|
81
|
+
}
|
|
82
|
+
const created = {
|
|
83
|
+
conversationKey: key,
|
|
84
|
+
peerSessionId: clean(peerSessionId),
|
|
85
|
+
remoteAgentId: clean(remoteAgentId),
|
|
86
|
+
selectedSkill: clean(selectedSkill) || 'friend-im',
|
|
87
|
+
createdAt: nowISO(),
|
|
88
|
+
updatedAt: nowISO(),
|
|
89
|
+
finalizedAt: '',
|
|
90
|
+
turns: [],
|
|
91
|
+
finalSummary: ''
|
|
92
|
+
}
|
|
93
|
+
conversations.set(key, created)
|
|
94
|
+
return clone(created)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getConversation(conversationKey) {
|
|
98
|
+
const existing = conversations.get(clean(conversationKey))
|
|
99
|
+
return existing ? clone(existing) : null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function appendTurn({
|
|
103
|
+
conversationKey,
|
|
104
|
+
peerSessionId,
|
|
105
|
+
requestId = '',
|
|
106
|
+
remoteAgentId,
|
|
107
|
+
selectedSkill,
|
|
108
|
+
turnIndex,
|
|
109
|
+
inboundText,
|
|
110
|
+
replyText,
|
|
111
|
+
decision,
|
|
112
|
+
stopReason = '',
|
|
113
|
+
finalize = false,
|
|
114
|
+
ownerSummary = ''
|
|
115
|
+
} = {}) {
|
|
116
|
+
const key = resolveConversationKey({ conversationKey, peerSessionId })
|
|
117
|
+
const current = conversations.get(key) || ensureConversation({
|
|
118
|
+
conversationKey: key,
|
|
119
|
+
peerSessionId,
|
|
120
|
+
remoteAgentId,
|
|
121
|
+
selectedSkill
|
|
122
|
+
})
|
|
123
|
+
const normalizedRequestId = clean(requestId)
|
|
124
|
+
const normalizedTurnFingerprint = stableTurnFingerprint({
|
|
125
|
+
turnIndex,
|
|
126
|
+
remoteAgentId,
|
|
127
|
+
inboundText,
|
|
128
|
+
replyText,
|
|
129
|
+
selectedSkill
|
|
130
|
+
})
|
|
131
|
+
if (normalizedRequestId && Array.isArray(current.turns)) {
|
|
132
|
+
const duplicate = current.turns.find((turn) => clean(turn.requestId) === normalizedRequestId)
|
|
133
|
+
if (duplicate) {
|
|
134
|
+
return clone(current)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (normalizedTurnFingerprint && Array.isArray(current.turns)) {
|
|
138
|
+
const duplicate = current.turns.find((turn) => clean(turn.turnFingerprint) === normalizedTurnFingerprint)
|
|
139
|
+
if (duplicate) {
|
|
140
|
+
return clone(current)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const normalized = {
|
|
144
|
+
...current,
|
|
145
|
+
conversationKey: key,
|
|
146
|
+
peerSessionId: clean(peerSessionId) || clean(current.peerSessionId),
|
|
147
|
+
remoteAgentId: clean(remoteAgentId) || clean(current.remoteAgentId),
|
|
148
|
+
selectedSkill: clean(selectedSkill) || clean(current.selectedSkill) || 'friend-im',
|
|
149
|
+
updatedAt: nowISO(),
|
|
150
|
+
turns: [
|
|
151
|
+
...(Array.isArray(current.turns) ? current.turns : []),
|
|
152
|
+
{
|
|
153
|
+
requestId: normalizedRequestId,
|
|
154
|
+
turnFingerprint: normalizedTurnFingerprint,
|
|
155
|
+
turnIndex: Number.parseInt(`${turnIndex ?? 1}`, 10) || 1,
|
|
156
|
+
inboundText: clean(inboundText),
|
|
157
|
+
replyText: clean(replyText),
|
|
158
|
+
decision: clean(decision),
|
|
159
|
+
stopReason: clean(stopReason),
|
|
160
|
+
finalize: Boolean(finalize),
|
|
161
|
+
ownerSummary: clean(ownerSummary),
|
|
162
|
+
createdAt: nowISO()
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
if (finalize) {
|
|
167
|
+
normalized.finalizedAt = nowISO()
|
|
168
|
+
normalized.finalSummary = clean(ownerSummary)
|
|
169
|
+
}
|
|
170
|
+
conversations.set(key, normalized)
|
|
171
|
+
return clone(normalized)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function transcript(conversationKey) {
|
|
175
|
+
return formatTranscript(conversations.get(clean(conversationKey))?.turns ?? [])
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function finalizeConversation(conversationKey, summary = '') {
|
|
179
|
+
const key = clean(conversationKey)
|
|
180
|
+
const current = conversations.get(key)
|
|
181
|
+
if (!current) {
|
|
182
|
+
return null
|
|
183
|
+
}
|
|
184
|
+
const updated = {
|
|
185
|
+
...current,
|
|
186
|
+
updatedAt: nowISO(),
|
|
187
|
+
finalizedAt: nowISO(),
|
|
188
|
+
finalSummary: clean(summary) || clean(current.finalSummary)
|
|
189
|
+
}
|
|
190
|
+
conversations.set(key, updated)
|
|
191
|
+
return clone(updated)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function endConversation(conversationKey) {
|
|
195
|
+
const key = clean(conversationKey)
|
|
196
|
+
const existing = conversations.get(key)
|
|
197
|
+
if (!existing) {
|
|
198
|
+
return null
|
|
199
|
+
}
|
|
200
|
+
conversations.delete(key)
|
|
201
|
+
return clone(existing)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function reset() {
|
|
205
|
+
conversations.clear()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
ensureConversation,
|
|
210
|
+
getConversation,
|
|
211
|
+
appendTurn,
|
|
212
|
+
transcript,
|
|
213
|
+
finalizeConversation,
|
|
214
|
+
endConversation,
|
|
215
|
+
reset,
|
|
216
|
+
snapshot() {
|
|
217
|
+
return {
|
|
218
|
+
activeConversations: conversations.size,
|
|
219
|
+
conversations: [...conversations.values()].map((value) => clone(value))
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|