ticlawk 0.1.16-dev.9 → 0.1.17-dev.1

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.
Files changed (42) hide show
  1. package/README.md +17 -3
  2. package/bin/ticlawk.mjs +255 -21
  3. package/package.json +1 -1
  4. package/src/adapters/ticlawk/api.mjs +350 -50
  5. package/src/adapters/ticlawk/credentials.mjs +41 -1
  6. package/src/adapters/ticlawk/index.mjs +248 -130
  7. package/src/adapters/ticlawk/wake-client.mjs +1 -1
  8. package/src/cli/agent-commands.mjs +715 -18
  9. package/src/core/agent-cli-handlers.mjs +556 -18
  10. package/src/core/agent-home.mjs +81 -1
  11. package/src/core/argv.mjs +11 -1
  12. package/src/core/events/worker-events.mjs +32 -36
  13. package/src/core/http.mjs +152 -0
  14. package/src/core/runtime-contract.mjs +0 -1
  15. package/src/core/runtime-env.mjs +7 -0
  16. package/src/core/runtime-support.mjs +130 -78
  17. package/src/runtimes/_shared/agent-handbook.mjs +45 -0
  18. package/src/runtimes/_shared/brand.mjs +2 -0
  19. package/src/runtimes/_shared/goal-step-prompt.mjs +98 -0
  20. package/src/runtimes/_shared/goal-task-protocol.mjs +50 -0
  21. package/src/runtimes/_shared/handbook/BASICS.md +27 -0
  22. package/src/runtimes/_shared/handbook/COLLABORATION.md +37 -0
  23. package/src/runtimes/_shared/handbook/COMMUNICATION.md +55 -0
  24. package/src/runtimes/_shared/handbook/DM_SCOPE.md +13 -0
  25. package/src/runtimes/_shared/handbook/GOAL_AUTHORITY.md +47 -0
  26. package/src/runtimes/_shared/handbook/GOAL_TASK_CORE.md +43 -0
  27. package/src/runtimes/_shared/handbook/GROUP_ADMIN_SCOPE.md +21 -0
  28. package/src/runtimes/_shared/handbook/GROUP_MEMBER_SCOPE.md +15 -0
  29. package/src/runtimes/_shared/handbook/SURFACES.md +41 -0
  30. package/src/runtimes/_shared/handbook/TASK_WORKER.md +14 -0
  31. package/src/runtimes/_shared/standing-prompt.mjs +124 -279
  32. package/src/runtimes/_shared/wake-prompt.mjs +268 -0
  33. package/src/runtimes/claude-code/index.mjs +19 -46
  34. package/src/runtimes/claude-code/session.mjs +2 -7
  35. package/src/runtimes/codex/index.mjs +115 -63
  36. package/src/runtimes/codex/session.mjs +2 -12
  37. package/src/runtimes/openclaw/index.mjs +11 -24
  38. package/src/runtimes/opencode/index.mjs +38 -60
  39. package/src/runtimes/opencode/session.mjs +12 -12
  40. package/src/runtimes/pi/index.mjs +38 -60
  41. package/src/runtimes/pi/session.mjs +9 -6
  42. package/ticlawk.mjs +0 -30
@@ -7,7 +7,8 @@
7
7
  * and forwards to the ticlawk backend using the connector API key.
8
8
  *
9
9
  * Targets are parsed in the daemon (not on the wire to backend) so the
10
- * CLI can speak `#<group>` / `dm:@<user>` / `#<group>:<short-msg-id>`
10
+ * CLI can speak `#<group>` / `dm:<conversation-id>` / `dm:@<user>` /
11
+ * `#<group>:<short-msg-id>`
11
12
  * while backend keeps a flat conversation_id contract.
12
13
  */
13
14
 
@@ -34,7 +35,7 @@ export function invalidateServerInfoCache(actingAgentId = null) {
34
35
  }
35
36
 
36
37
  /**
37
- * Parse a target string into { conversationId, threadRootMsgId } using a
38
+ * Parse a target string into { conversationId, replyToMessageId } using a
38
39
  * cached server-info lookup. Returns null fields if the target cannot be
39
40
  * resolved; callers should treat that as a 404.
40
41
  *
@@ -43,29 +44,29 @@ export function invalidateServerInfoCache(actingAgentId = null) {
43
44
  * dm:@<handle> -> find DM conversation whose other member is <handle>
44
45
  * #<uuid> -> conversation_id = <uuid>
45
46
  * #<group-name> -> find group conversation by name
46
- * <foo>:<short-msg-id> -> thread under <foo>, root = first message whose
47
+ * <foo>:<short-msg-id> -> replies under <foo>, root = first message whose
47
48
  * id startsWith <short-msg-id>
48
49
  */
49
50
  export async function resolveTarget(actingAgentId, target) {
50
- if (!target) return { conversationId: null, threadRootMsgId: null, error: 'target is required' };
51
+ if (!target) return { conversationId: null, replyToMessageId: null, error: 'target is required' };
51
52
 
52
- // Strip optional thread suffix.
53
+ // Strip optional message-reply suffix.
53
54
  let base = target;
54
- let threadShort = null;
55
+ let replyToMessageId = null;
55
56
  const colonIdx = target.indexOf(':', target.startsWith('dm:') ? 3 : 1);
56
57
  if (colonIdx > 0 && colonIdx < target.length - 1) {
57
- threadShort = target.slice(colonIdx + 1);
58
+ replyToMessageId = target.slice(colonIdx + 1);
58
59
  base = target.slice(0, colonIdx);
59
60
  }
60
61
 
61
62
  if (/^[0-9a-f-]{36}$/i.test(base)) {
62
- return { conversationId: base, threadRootMsgId: threadShort, error: null };
63
+ return { conversationId: base, replyToMessageId, error: null };
63
64
  }
64
65
  if (base.startsWith('dm:') && /^[0-9a-f-]{36}$/i.test(base.slice(3))) {
65
- return { conversationId: base.slice(3), threadRootMsgId: threadShort, error: null };
66
+ return { conversationId: base.slice(3), replyToMessageId, error: null };
66
67
  }
67
68
  if (base.startsWith('#') && /^[0-9a-f-]{36}$/i.test(base.slice(1))) {
68
- return { conversationId: base.slice(1), threadRootMsgId: threadShort, error: null };
69
+ return { conversationId: base.slice(1), replyToMessageId, error: null };
69
70
  }
70
71
 
71
72
  const info = await getCachedServerInfo(actingAgentId);
@@ -76,19 +77,30 @@ export async function resolveTarget(actingAgentId, target) {
76
77
  const match = convs.find((c) =>
77
78
  c.type === 'dm' && (String(c.display_name || c.name || '').toLowerCase() === handle)
78
79
  );
79
- if (match) return { conversationId: match.id, threadRootMsgId: threadShort, error: null };
80
- return { conversationId: null, threadRootMsgId: null, error: `unknown dm target: ${target}` };
80
+ if (match) return { conversationId: match.id, replyToMessageId, error: null };
81
+ return { conversationId: null, replyToMessageId: null, error: `unknown dm target: ${target}` };
81
82
  }
82
83
  if (base.startsWith('#')) {
83
84
  const name = base.slice(1).toLowerCase();
84
85
  const match = convs.find((c) =>
85
86
  c.type === 'group' && (String(c.name || c.display_name || '').toLowerCase() === name)
86
87
  );
87
- if (match) return { conversationId: match.id, threadRootMsgId: threadShort, error: null };
88
- return { conversationId: null, threadRootMsgId: null, error: `unknown group target: ${target}` };
88
+ if (match) return { conversationId: match.id, replyToMessageId, error: null };
89
+
90
+ if (info?.agent?.is_cos) {
91
+ const workstreams = await api.listWorkstreams({ actingAgentId });
92
+ const workstreamMatch = workstreams.find((c) =>
93
+ String(c.name || c.display_name || '').toLowerCase() === name
94
+ );
95
+ if (workstreamMatch) {
96
+ return { conversationId: workstreamMatch.id, replyToMessageId, error: null };
97
+ }
98
+ }
99
+
100
+ return { conversationId: null, replyToMessageId: null, error: `unknown group target: ${target}` };
89
101
  }
90
102
 
91
- return { conversationId: null, threadRootMsgId: null, error: `invalid target syntax: ${target}` };
103
+ return { conversationId: null, replyToMessageId: null, error: `invalid target syntax: ${target}` };
92
104
  }
93
105
 
94
106
  function getActingAgentId(req, body = {}) {
@@ -109,6 +121,15 @@ function getRuntimeHostId(req, body = {}) {
109
121
  return null;
110
122
  }
111
123
 
124
+ function getCurrentConversationId(req, body = {}) {
125
+ const fromHeader = req.headers['x-ticlawk-current-conversation-id'];
126
+ if (typeof fromHeader === 'string' && fromHeader.trim()) return fromHeader.trim();
127
+ if (typeof body?.current_conversation_id === 'string' && body.current_conversation_id.trim()) {
128
+ return body.current_conversation_id.trim();
129
+ }
130
+ return null;
131
+ }
132
+
112
133
  function validateActingAgent(actingAgentId, ctx) {
113
134
  if (!actingAgentId) {
114
135
  return { ok: false, status: 400, error: 'TICLAWK_RUNTIME_AGENT_ID required (passed via X-Ticlawk-Acting-Agent-Id or body.acting_as_agent_id)' };
@@ -123,6 +144,16 @@ function validateActingAgent(actingAgentId, ctx) {
123
144
  return { ok: true };
124
145
  }
125
146
 
147
+ function validateAgentResponsePhase(metadata) {
148
+ const phase = metadata?.agent_response_phase;
149
+ if (phase == null) return { ok: true, phase: null };
150
+ const normalized = String(phase).trim().toLowerCase();
151
+ if (normalized === 'progress' || normalized === 'final') {
152
+ return { ok: true, phase: normalized };
153
+ }
154
+ return { ok: false, error: 'metadata.agent_response_phase must be progress or final' };
155
+ }
156
+
126
157
  export async function handleMessageSend(req, body, ctx) {
127
158
  const actingAgentId = getActingAgentId(req, body);
128
159
  const v = validateActingAgent(actingAgentId, ctx);
@@ -132,22 +163,45 @@ export async function handleMessageSend(req, body, ctx) {
132
163
  if (!text) return { status: 400, body: { error: 'text is required' } };
133
164
 
134
165
  let conversationId = body?.conversation_id || null;
135
- let threadRootMsgId = null;
166
+ let targetReplyToMessageId = null;
136
167
  if (!conversationId && body?.target) {
137
168
  const resolved = await resolveTarget(actingAgentId, String(body.target));
138
169
  if (resolved.error) {
139
170
  return { status: 404, body: { error: resolved.error } };
140
171
  }
141
172
  conversationId = resolved.conversationId;
142
- threadRootMsgId = resolved.threadRootMsgId;
173
+ targetReplyToMessageId = resolved.replyToMessageId;
143
174
  }
144
175
  if (!conversationId) {
145
176
  return { status: 400, body: { error: 'target or conversation_id is required' } };
146
177
  }
147
178
 
179
+ const currentConversationId = getCurrentConversationId(req, body);
180
+ if (currentConversationId && currentConversationId !== conversationId && !body?.allow_cross_target) {
181
+ debugLog('agent-cli', 'send.blocked-cross-target', {
182
+ actingAgentId,
183
+ currentConversationId,
184
+ conversationId,
185
+ target: body?.target || null,
186
+ });
187
+ return {
188
+ status: 409,
189
+ body: {
190
+ error: 'refusing to send to a different conversation from the current runtime turn',
191
+ current_conversation_id: currentConversationId,
192
+ target_conversation_id: conversationId,
193
+ hint: 'Use --allow-cross-target only for an intentional cross-conversation send.',
194
+ },
195
+ };
196
+ }
197
+
148
198
  const mediaAssetIds = Array.isArray(body?.media_asset_ids)
149
199
  ? body.media_asset_ids.map((v) => String(v).trim()).filter(Boolean)
150
200
  : [];
201
+ const metadata = body?.metadata;
202
+ const phaseRes = validateAgentResponsePhase(metadata);
203
+ if (!phaseRes.ok) return { status: 400, body: { error: phaseRes.error } };
204
+ if (metadata && phaseRes.phase) metadata.agent_response_phase = phaseRes.phase;
151
205
 
152
206
  try {
153
207
  const data = await api.sendAgentMessage({
@@ -155,15 +209,17 @@ export async function handleMessageSend(req, body, ctx) {
155
209
  conversationId,
156
210
  text,
157
211
  seenUpToSeq: body?.seen_up_to_seq,
158
- replyToMessageId: body?.reply_to_message_id || threadRootMsgId || null,
212
+ replyToMessageId: body?.reply_to_message_id || targetReplyToMessageId || null,
159
213
  runtimeHostId: getRuntimeHostId(req, body),
160
214
  mediaAssetIds,
215
+ metadata,
161
216
  });
162
217
  debugLog('agent-cli', 'send.ok', {
163
218
  actingAgentId,
164
219
  conversationId,
165
220
  messageId: data?.id,
166
221
  seq: data?.seq,
222
+ agentResponsePhase: phaseRes.phase,
167
223
  bodyChars: text.length,
168
224
  });
169
225
  return { status: 200, body: { ok: true, data } };
@@ -229,6 +285,7 @@ export async function handleTaskCreate(req, body, ctx) {
229
285
  conversationId,
230
286
  text,
231
287
  title: body?.title ?? null,
288
+ assignAgentId: body?.assign_agent_id || body?.assignee_agent_id || null,
232
289
  });
233
290
  debugLog('agent-cli', 'task.create', {
234
291
  actingAgentId,
@@ -330,6 +387,127 @@ export async function handleTaskUpdate(req, body, ctx) {
330
387
  }
331
388
  }
332
389
 
390
+ export async function handleGoalReport(req, body, ctx) {
391
+ const actingAgentId = getActingAgentId(req, body);
392
+ const v = validateActingAgent(actingAgentId, ctx);
393
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
394
+ const conversationId = body?.conversation_id || getCurrentConversationId(req, body);
395
+ if (!conversationId) return { status: 400, body: { error: 'conversation_id is required' } };
396
+ if (!body?.transition_id) return { status: 400, body: { error: 'transition_id is required' } };
397
+ if (!body?.outcome) return { status: 400, body: { error: 'outcome is required' } };
398
+ try {
399
+ const data = await api.reportGoalTransition({
400
+ actingAgentId,
401
+ conversationId,
402
+ transitionId: body.transition_id,
403
+ outcome: body.outcome,
404
+ detail: body.detail || null,
405
+ currentTaskId: body.current_task_id || null,
406
+ });
407
+ debugLog('agent-cli', 'goal.report', {
408
+ actingAgentId,
409
+ conversationId,
410
+ outcome: body.outcome,
411
+ state: data?.state,
412
+ });
413
+ return { status: data?.ok ? 200 : 400, body: data };
414
+ } catch (err) {
415
+ return { status: err?.status || 500, body: { error: err?.message || 'goal report failed' } };
416
+ }
417
+ }
418
+
419
+ export async function handleGoalChanged(req, body, ctx) {
420
+ const actingAgentId = getActingAgentId(req, body);
421
+ const v = validateActingAgent(actingAgentId, ctx);
422
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
423
+ const conversationId = body?.conversation_id || getCurrentConversationId(req, body);
424
+ if (!conversationId) return { status: 400, body: { error: 'conversation_id is required' } };
425
+ try {
426
+ const data = await api.noteGoalChanged({ actingAgentId, conversationId });
427
+ debugLog('agent-cli', 'goal.changed', {
428
+ actingAgentId,
429
+ conversationId,
430
+ goalVersion: data?.goal_version,
431
+ });
432
+ return { status: data?.ok ? 200 : 400, body: data };
433
+ } catch (err) {
434
+ return { status: err?.status || 500, body: { error: err?.message || 'goal changed failed' } };
435
+ }
436
+ }
437
+
438
+ export async function handleApprovalRequest(req, body, ctx) {
439
+ const actingAgentId = getActingAgentId(req, body);
440
+ const v = validateActingAgent(actingAgentId, ctx);
441
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
442
+ const conversationId = body?.conversation_id || getCurrentConversationId(req, body);
443
+ if (!conversationId) return { status: 400, body: { error: 'conversation_id is required' } };
444
+ if (!body?.title) return { status: 400, body: { error: 'title is required' } };
445
+ try {
446
+ const data = await api.requestGoalApproval({
447
+ actingAgentId,
448
+ conversationId,
449
+ title: body.title,
450
+ detail: body.detail || null,
451
+ ttlSeconds: body.ttl_seconds || null,
452
+ });
453
+ debugLog('agent-cli', 'approval.request', {
454
+ actingAgentId,
455
+ conversationId,
456
+ requestId: data?.request_id,
457
+ });
458
+ return { status: data?.ok ? 200 : 400, body: data };
459
+ } catch (err) {
460
+ return { status: err?.status || 500, body: { error: err?.message || 'approval request failed' } };
461
+ }
462
+ }
463
+
464
+ export async function handleApprovalList(req, query, ctx) {
465
+ const actingAgentId = getActingAgentId(req, query);
466
+ const v = validateActingAgent(actingAgentId, ctx);
467
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
468
+ let conversationId = query?.conversation_id || null;
469
+ if (!conversationId && query?.target) {
470
+ const resolved = await resolveTarget(actingAgentId, String(query.target));
471
+ if (!resolved.error) conversationId = resolved.conversationId;
472
+ }
473
+ if (!conversationId) return { status: 400, body: { error: 'conversation_id is required' } };
474
+ try {
475
+ const data = await api.listGoalApprovals({ actingAgentId, conversationId });
476
+ return { status: 200, body: { data } };
477
+ } catch (err) {
478
+ return { status: err?.status || 500, body: { error: err?.message || 'approval list failed' } };
479
+ }
480
+ }
481
+
482
+ export async function handleApprovalResolve(req, body, ctx) {
483
+ const actingAgentId = getActingAgentId(req, body);
484
+ const v = validateActingAgent(actingAgentId, ctx);
485
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
486
+ if (!body?.request_id) return { status: 400, body: { error: 'request_id is required' } };
487
+ if (body?.decision !== 'granted' && body?.decision !== 'rejected') {
488
+ return { status: 400, body: { error: 'decision must be granted or rejected' } };
489
+ }
490
+ try {
491
+ const data = await api.resolveGoalApproval({
492
+ actingAgentId,
493
+ requestId: body.request_id,
494
+ decision: body.decision,
495
+ originalText: body.original_text || null,
496
+ confidence: body.confidence ?? null,
497
+ sourceMessageId: body.source_message_id || null,
498
+ });
499
+ debugLog('agent-cli', 'approval.resolve', {
500
+ actingAgentId,
501
+ requestId: body.request_id,
502
+ decision: body.decision,
503
+ resumed: data?.resumed,
504
+ });
505
+ return { status: data?.ok ? 200 : 400, body: data };
506
+ } catch (err) {
507
+ return { status: err?.status || 500, body: { error: err?.message || 'approval resolve failed' } };
508
+ }
509
+ }
510
+
333
511
  export async function handleTaskList(req, query, ctx) {
334
512
  const actingAgentId = getActingAgentId(req, query);
335
513
  const v = validateActingAgent(actingAgentId, ctx);
@@ -716,6 +894,366 @@ export async function handleGroupMembersRemove(req, body, ctx) {
716
894
  }
717
895
  }
718
896
 
897
+ export async function handleWorkstreamCreate(req, body, ctx) {
898
+ const actingAgentId = getActingAgentId(req, body);
899
+ const v = validateActingAgent(actingAgentId, ctx);
900
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
901
+ const name = String(body?.name || '').trim();
902
+ if (!name) return { status: 400, body: { error: 'name is required' } };
903
+ try {
904
+ const data = await api.createWorkstream({
905
+ actingAgentId,
906
+ name,
907
+ description: body?.description || null,
908
+ charter: body?.charter ?? null,
909
+ memberAgentIds: Array.isArray(body?.member_agent_ids) ? body.member_agent_ids : [],
910
+ });
911
+ invalidateServerInfoCache(actingAgentId);
912
+ debugLog('agent-cli', 'workstream.create', {
913
+ actingAgentId, conversationId: data?.conversation?.id,
914
+ });
915
+ return { status: 200, body: data };
916
+ } catch (err) {
917
+ return { status: err?.status || 500, body: { error: err?.message || 'workstream create failed' } };
918
+ }
919
+ }
920
+
921
+ export async function handleWorkstreamDelete(req, body, ctx) {
922
+ const actingAgentId = getActingAgentId(req, body);
923
+ const v = validateActingAgent(actingAgentId, ctx);
924
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
925
+ let conversationId = body?.conversation_id || null;
926
+ if (!conversationId && body?.target) {
927
+ const resolved = await resolveTarget(actingAgentId, String(body.target));
928
+ if (resolved.error) return { status: 404, body: { error: resolved.error } };
929
+ conversationId = resolved.conversationId;
930
+ }
931
+ if (!conversationId) return { status: 400, body: { error: 'target or conversation_id is required' } };
932
+ try {
933
+ const data = await api.deleteWorkstream({ actingAgentId, conversationId });
934
+ invalidateServerInfoCache(actingAgentId);
935
+ return { status: 200, body: data };
936
+ } catch (err) {
937
+ return { status: err?.status || 500, body: { error: err?.message || 'workstream delete failed' } };
938
+ }
939
+ }
940
+
941
+ export async function handleWorkstreamList(req, query, ctx) {
942
+ const actingAgentId = getActingAgentId(req, query);
943
+ const v = validateActingAgent(actingAgentId, ctx);
944
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
945
+ try {
946
+ const data = await api.listWorkstreams({ actingAgentId });
947
+ return { status: 200, body: { data } };
948
+ } catch (err) {
949
+ return { status: err?.status || 500, body: { error: err?.message || 'workstream list failed' } };
950
+ }
951
+ }
952
+
953
+ export async function handleAgentList(req, query, ctx) {
954
+ const actingAgentId = getActingAgentId(req, query);
955
+ const v = validateActingAgent(actingAgentId, ctx);
956
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
957
+ try {
958
+ const data = await api.listAgentSlots({ actingAgentId });
959
+ return { status: 200, body: { data } };
960
+ } catch (err) {
961
+ return { status: err?.status || 500, body: { error: err?.message || 'agent list failed' } };
962
+ }
963
+ }
964
+
965
+ export async function handleAgentCreate(req, body, ctx) {
966
+ const actingAgentId = getActingAgentId(req, body);
967
+ const v = validateActingAgent(actingAgentId, ctx);
968
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
969
+ const name = String(body?.name || '').trim();
970
+ const runtime = String(body?.runtime || '').trim();
971
+ if (!name) return { status: 400, body: { error: 'name is required' } };
972
+ if (!runtime) return { status: 400, body: { error: 'runtime is required' } };
973
+ try {
974
+ const data = await api.createAgentSlot({
975
+ actingAgentId,
976
+ name,
977
+ runtime,
978
+ description: body?.description || null,
979
+ displayName: body?.display_name || null,
980
+ model: body?.model || null,
981
+ });
982
+ return { status: 200, body: data };
983
+ } catch (err) {
984
+ return { status: err?.status || 500, body: { error: err?.message || 'agent create failed' } };
985
+ }
986
+ }
987
+
988
+ export async function handleAgentDelete(req, body, ctx) {
989
+ const actingAgentId = getActingAgentId(req, body);
990
+ const v = validateActingAgent(actingAgentId, ctx);
991
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
992
+ const agentId = String(body?.agent_id || '').trim();
993
+ if (!agentId) return { status: 400, body: { error: 'agent_id is required' } };
994
+ try {
995
+ const data = await api.archiveAgentSlot({ actingAgentId, agentId });
996
+ return { status: 200, body: data };
997
+ } catch (err) {
998
+ return { status: err?.status || 500, body: { error: err?.message || 'agent delete failed' } };
999
+ }
1000
+ }
1001
+
1002
+ export async function handleServiceCreate(req, body, ctx) {
1003
+ const actingAgentId = getActingAgentId(req, body);
1004
+ const v = validateActingAgent(actingAgentId, ctx);
1005
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
1006
+ const name = String(body?.name || '').trim();
1007
+ if (!name) return { status: 400, body: { error: 'name is required' } };
1008
+ if (!body?.endpoint_config || typeof body.endpoint_config !== 'object') {
1009
+ return { status: 400, body: { error: 'endpoint_config is required' } };
1010
+ }
1011
+ try {
1012
+ const data = await api.createService({
1013
+ actingAgentId, name,
1014
+ description: body?.description || null,
1015
+ contractSchema: body?.contract_schema ?? null,
1016
+ endpointConfig: body.endpoint_config,
1017
+ });
1018
+ return { status: 200, body: data };
1019
+ } catch (err) {
1020
+ return { status: err?.status || 500, body: { error: err?.message || 'service create failed' } };
1021
+ }
1022
+ }
1023
+
1024
+ export async function handleServiceUpdate(req, body, ctx) {
1025
+ const actingAgentId = getActingAgentId(req, body);
1026
+ const v = validateActingAgent(actingAgentId, ctx);
1027
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
1028
+ const serviceId = String(body?.service_id || '').trim();
1029
+ if (!serviceId) return { status: 400, body: { error: 'service_id is required' } };
1030
+ const patch = {};
1031
+ if ('description' in (body || {})) patch.description = body.description;
1032
+ if ('contract_schema' in (body || {})) patch.contract_schema = body.contract_schema;
1033
+ if ('endpoint_config' in (body || {})) patch.endpoint_config = body.endpoint_config;
1034
+ if ('status' in (body || {})) patch.status = body.status;
1035
+ try {
1036
+ const data = await api.updateService({ actingAgentId, serviceId, ...patch });
1037
+ return { status: 200, body: data };
1038
+ } catch (err) {
1039
+ return { status: err?.status || 500, body: { error: err?.message || 'service update failed' } };
1040
+ }
1041
+ }
1042
+
1043
+ export async function handleServiceDelete(req, body, ctx) {
1044
+ const actingAgentId = getActingAgentId(req, body);
1045
+ const v = validateActingAgent(actingAgentId, ctx);
1046
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
1047
+ const serviceId = String(body?.service_id || '').trim();
1048
+ if (!serviceId) return { status: 400, body: { error: 'service_id is required' } };
1049
+ try {
1050
+ const data = await api.deleteService({ actingAgentId, serviceId });
1051
+ return { status: 200, body: data };
1052
+ } catch (err) {
1053
+ return { status: err?.status || 500, body: { error: err?.message || 'service delete failed' } };
1054
+ }
1055
+ }
1056
+
1057
+ export async function handleServiceList(req, query, ctx) {
1058
+ const actingAgentId = getActingAgentId(req, query);
1059
+ const v = validateActingAgent(actingAgentId, ctx);
1060
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
1061
+ try {
1062
+ const data = await api.listServices({ actingAgentId });
1063
+ return { status: 200, body: { data } };
1064
+ } catch (err) {
1065
+ return { status: err?.status || 500, body: { error: err?.message || 'service list failed' } };
1066
+ }
1067
+ }
1068
+
1069
+ export async function handleServiceInfo(req, query, ctx) {
1070
+ const actingAgentId = getActingAgentId(req, query);
1071
+ const v = validateActingAgent(actingAgentId, ctx);
1072
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
1073
+ const name = String(query?.name || '').trim();
1074
+ if (!name) return { status: 400, body: { error: 'name is required' } };
1075
+ try {
1076
+ const data = await api.getServiceInfo({ actingAgentId, name });
1077
+ return { status: 200, body: data };
1078
+ } catch (err) {
1079
+ return { status: err?.status || 500, body: { error: err?.message || 'service info failed' } };
1080
+ }
1081
+ }
1082
+
1083
+ export async function handleServiceCall(req, body, ctx) {
1084
+ const actingAgentId = getActingAgentId(req, body);
1085
+ const v = validateActingAgent(actingAgentId, ctx);
1086
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
1087
+ const name = String(body?.name || '').trim();
1088
+ if (!name) return { status: 400, body: { error: 'name is required' } };
1089
+ try {
1090
+ const data = await api.callService({
1091
+ actingAgentId, name,
1092
+ input: body?.input ?? null,
1093
+ });
1094
+ return { status: 200, body: data };
1095
+ } catch (err) {
1096
+ return { status: err?.status || 500, body: { error: err?.message || 'service call failed' } };
1097
+ }
1098
+ }
1099
+
1100
+ export async function handleBriefingGet(req, query, ctx) {
1101
+ const actingAgentId = getActingAgentId(req, query);
1102
+ const v = validateActingAgent(actingAgentId, ctx);
1103
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
1104
+ const briefingId = String(query?.id || '').trim();
1105
+ if (!briefingId) return { status: 400, body: { error: 'id is required' } };
1106
+ try {
1107
+ const data = await api.getBriefing({ actingAgentId, briefingId });
1108
+ return { status: 200, body: data };
1109
+ } catch (err) {
1110
+ return { status: err?.status || 500, body: { error: err?.message || 'briefing get failed' } };
1111
+ }
1112
+ }
1113
+
1114
+ export async function handleBriefingPublish(req, body, ctx) {
1115
+ const actingAgentId = getActingAgentId(req, body);
1116
+ const v = validateActingAgent(actingAgentId, ctx);
1117
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
1118
+ const bodyText = typeof body?.body_text === 'string' && body.body_text.trim() ? body.body_text.trim() : null;
1119
+ const attachmentAssetId = typeof body?.attachment_asset_id === 'string' && body.attachment_asset_id.trim()
1120
+ ? body.attachment_asset_id.trim()
1121
+ : null;
1122
+ const responseMode = typeof body?.response_mode === 'string' && body.response_mode.trim()
1123
+ ? body.response_mode.trim().toLowerCase()
1124
+ : 'info';
1125
+ if (!bodyText) {
1126
+ return { status: 400, body: { error: 'body_text is required' } };
1127
+ }
1128
+ if (bodyText && bodyText.length > 140) {
1129
+ return { status: 400, body: { error: 'body_text must be ≤140 chars' } };
1130
+ }
1131
+ if (!['info', 'approval'].includes(responseMode)) {
1132
+ return { status: 400, body: { error: 'response_mode must be info or approval' } };
1133
+ }
1134
+ const currentConversationId = getCurrentConversationId(req, body);
1135
+ try {
1136
+ const data = await api.publishBriefing({
1137
+ actingAgentId,
1138
+ bodyText,
1139
+ attachmentAssetId,
1140
+ currentConversationId,
1141
+ responseMode,
1142
+ });
1143
+ return { status: 200, body: data };
1144
+ } catch (err) {
1145
+ return { status: err?.status || 500, body: { error: err?.message || 'briefing publish failed' } };
1146
+ }
1147
+ }
1148
+
1149
+ export async function handleCredentialRequest(req, body, ctx) {
1150
+ const actingAgentId = getActingAgentId(req, body);
1151
+ const v = validateActingAgent(actingAgentId, ctx);
1152
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
1153
+ const name = String(body?.name || '').trim();
1154
+ if (!name) return { status: 400, body: { error: 'name is required' } };
1155
+ let workstreamId = body?.workstream_id || null;
1156
+ if (!workstreamId && body?.target) {
1157
+ const resolved = await resolveTarget(actingAgentId, String(body.target));
1158
+ if (resolved.error) return { status: 404, body: { error: resolved.error } };
1159
+ workstreamId = resolved.conversationId;
1160
+ }
1161
+ try {
1162
+ const data = await api.requestCredential({
1163
+ actingAgentId,
1164
+ name,
1165
+ description: body?.description || null,
1166
+ workstreamId,
1167
+ });
1168
+ return { status: 200, body: data };
1169
+ } catch (err) {
1170
+ return { status: err?.status || 500, body: { error: err?.message || 'credential request failed' } };
1171
+ }
1172
+ }
1173
+
1174
+ export async function handleWorkstreamDashboardSet(req, body, ctx) {
1175
+ const actingAgentId = getActingAgentId(req, body);
1176
+ const v = validateActingAgent(actingAgentId, ctx);
1177
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
1178
+ let conversationId = body?.conversation_id || null;
1179
+ if (!conversationId && body?.target) {
1180
+ const resolved = await resolveTarget(actingAgentId, String(body.target));
1181
+ if (resolved.error) return { status: 404, body: { error: resolved.error } };
1182
+ conversationId = resolved.conversationId;
1183
+ }
1184
+ if (!conversationId) return { status: 400, body: { error: 'target or conversation_id is required' } };
1185
+ const payload = { actingAgentId, conversationId };
1186
+ if ('data_json' in (body || {})) payload.dataJson = body.data_json;
1187
+ if ('html_template' in (body || {})) payload.htmlTemplate = body.html_template;
1188
+ try {
1189
+ const data = await api.setWorkstreamDashboard(payload);
1190
+ return { status: 200, body: data };
1191
+ } catch (err) {
1192
+ return { status: err?.status || 500, body: { error: err?.message || 'dashboard set failed' } };
1193
+ }
1194
+ }
1195
+
1196
+ export async function handleWorkstreamDashboardGet(req, query, ctx) {
1197
+ const actingAgentId = getActingAgentId(req, query);
1198
+ const v = validateActingAgent(actingAgentId, ctx);
1199
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
1200
+ let conversationId = query?.conversation_id || null;
1201
+ if (!conversationId && query?.target) {
1202
+ const resolved = await resolveTarget(actingAgentId, String(query.target));
1203
+ if (resolved.error) return { status: 404, body: { error: resolved.error } };
1204
+ conversationId = resolved.conversationId;
1205
+ }
1206
+ if (!conversationId) return { status: 400, body: { error: 'target or conversation_id is required' } };
1207
+ try {
1208
+ const data = await api.getWorkstreamDashboard({ actingAgentId, conversationId });
1209
+ return { status: 200, body: data };
1210
+ } catch (err) {
1211
+ return { status: err?.status || 500, body: { error: err?.message || 'dashboard get failed' } };
1212
+ }
1213
+ }
1214
+
1215
+ export async function handleWorkstreamCharterGet(req, query, ctx) {
1216
+ const actingAgentId = getActingAgentId(req, query);
1217
+ const v = validateActingAgent(actingAgentId, ctx);
1218
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
1219
+ let conversationId = query?.conversation_id || null;
1220
+ if (!conversationId && query?.target) {
1221
+ const resolved = await resolveTarget(actingAgentId, String(query.target));
1222
+ if (resolved.error) return { status: 404, body: { error: resolved.error } };
1223
+ conversationId = resolved.conversationId;
1224
+ }
1225
+ if (!conversationId) return { status: 400, body: { error: 'target or conversation_id is required' } };
1226
+ try {
1227
+ const data = await api.getWorkstreamCharter({ actingAgentId, conversationId });
1228
+ return { status: 200, body: data };
1229
+ } catch (err) {
1230
+ return { status: err?.status || 500, body: { error: err?.message || 'charter get failed' } };
1231
+ }
1232
+ }
1233
+
1234
+ export async function handleWorkstreamCharterSet(req, body, ctx) {
1235
+ const actingAgentId = getActingAgentId(req, body);
1236
+ const v = validateActingAgent(actingAgentId, ctx);
1237
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
1238
+ let conversationId = body?.conversation_id || null;
1239
+ if (!conversationId && body?.target) {
1240
+ const resolved = await resolveTarget(actingAgentId, String(body.target));
1241
+ if (resolved.error) return { status: 404, body: { error: resolved.error } };
1242
+ conversationId = resolved.conversationId;
1243
+ }
1244
+ if (!conversationId) return { status: 400, body: { error: 'target or conversation_id is required' } };
1245
+ const charter = typeof body?.charter === 'string' ? body.charter : null;
1246
+ try {
1247
+ const data = await api.setWorkstreamCharter({ actingAgentId, conversationId, charter });
1248
+ debugLog('agent-cli', 'workstream.charter.set', {
1249
+ actingAgentId, conversationId, len: charter?.length ?? 0,
1250
+ });
1251
+ return { status: 200, body: data };
1252
+ } catch (err) {
1253
+ return { status: err?.status || 500, body: { error: err?.message || 'charter set failed' } };
1254
+ }
1255
+ }
1256
+
719
1257
  export async function handleServerInfo(req, query, ctx) {
720
1258
  const actingAgentId = getActingAgentId(req, query);
721
1259
  const v = validateActingAgent(actingAgentId, ctx);