@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/bin/a2-cli.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runA2Cli } from '../a2_cli.mjs'
4
+
5
+ runA2Cli(process.argv.slice(2)).catch((error) => {
6
+ console.error(error.message)
7
+ process.exit(1)
8
+ })
@@ -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
+ }