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
@@ -30,8 +30,9 @@ export function getConnectorWsUrl() {
30
30
  return DEFAULT_CONNECTOR_WS_URL;
31
31
  }
32
32
 
33
- // Agent event writes must preserve per-agent order while still allowing
34
- // different agents to proceed concurrently.
33
+ // Non-terminal event writes are serialized per agent for stable logs.
34
+ // Terminal turn events bypass this queue so telemetry cannot hold up the
35
+ // delivery lifecycle.
35
36
  const agentEventQueues = new Map(); // agentId -> Promise
36
37
 
37
38
  export class TiclawkUpdateRequiredError extends Error {
@@ -151,13 +152,13 @@ export async function updateChannel(id, updates) {
151
152
 
152
153
  // ── Deliveries (replaces legacy message_jobs poll/ack) ──
153
154
 
154
- export async function claimPendingDeliveries(hostId, limit = 5, excludedAgentIds = []) {
155
+ export async function claimPendingDeliveries(hostId, limit = 5, excludedChannels = []) {
155
156
  const { data } = await apiFetch('/api/agent/deliveries/claim-pending', {
156
157
  method: 'POST',
157
158
  body: JSON.stringify(withTiclawkVersion({
158
159
  runtime_host_id: hostId,
159
160
  limit,
160
- excluded_agent_ids: excludedAgentIds,
161
+ excluded_channels: excludedChannels,
161
162
  })),
162
163
  });
163
164
  return data || [];
@@ -188,6 +189,7 @@ export async function sendAgentMessage({
188
189
  runtimeHostId,
189
190
  visibility,
190
191
  mediaAssetIds,
192
+ metadata,
191
193
  }) {
192
194
  const { data } = await apiFetch('/api/agent/messages/send', {
193
195
  method: 'POST',
@@ -200,6 +202,7 @@ export async function sendAgentMessage({
200
202
  runtime_host_id: runtimeHostId ?? null,
201
203
  visibility: visibility || null,
202
204
  media_asset_ids: Array.isArray(mediaAssetIds) && mediaAssetIds.length > 0 ? mediaAssetIds : undefined,
205
+ metadata: metadata ?? undefined,
203
206
  }),
204
207
  });
205
208
  return data || null;
@@ -222,7 +225,7 @@ export async function readAgentMessages({
222
225
  return data || [];
223
226
  }
224
227
 
225
- export async function createAgentTask({ actingAgentId, conversationId, text, title }) {
228
+ export async function createAgentTask({ actingAgentId, conversationId, text, title, assignAgentId }) {
226
229
  return apiFetch('/api/agent/tasks/create', {
227
230
  method: 'POST',
228
231
  body: JSON.stringify({
@@ -230,6 +233,7 @@ export async function createAgentTask({ actingAgentId, conversationId, text, tit
230
233
  conversation_id: conversationId,
231
234
  text,
232
235
  title: title ?? null,
236
+ assign_agent_id: assignAgentId ?? null,
233
237
  }),
234
238
  });
235
239
  }
@@ -282,6 +286,71 @@ export async function updateAgentTask({ actingAgentId, taskId, status }) {
282
286
  });
283
287
  }
284
288
 
289
+ export async function reportGoalTransition({
290
+ actingAgentId, conversationId, transitionId, outcome, detail, currentTaskId,
291
+ }) {
292
+ return apiFetch('/api/agent/goal/report', {
293
+ method: 'POST',
294
+ body: JSON.stringify({
295
+ acting_as_agent_id: actingAgentId,
296
+ conversation_id: conversationId,
297
+ transition_id: transitionId,
298
+ outcome,
299
+ detail: detail ?? null,
300
+ current_task_id: currentTaskId ?? null,
301
+ }),
302
+ });
303
+ }
304
+
305
+ export async function noteGoalChanged({ actingAgentId, conversationId }) {
306
+ return apiFetch('/api/agent/goal/changed', {
307
+ method: 'POST',
308
+ body: JSON.stringify({
309
+ acting_as_agent_id: actingAgentId,
310
+ conversation_id: conversationId,
311
+ }),
312
+ });
313
+ }
314
+
315
+ export async function requestGoalApproval({
316
+ actingAgentId, conversationId, title, detail, ttlSeconds,
317
+ }) {
318
+ return apiFetch('/api/agent/approval/request', {
319
+ method: 'POST',
320
+ body: JSON.stringify({
321
+ acting_as_agent_id: actingAgentId,
322
+ conversation_id: conversationId,
323
+ title,
324
+ detail: detail ?? null,
325
+ ttl_seconds: ttlSeconds ?? null,
326
+ }),
327
+ });
328
+ }
329
+
330
+ export async function listGoalApprovals({ actingAgentId, conversationId }) {
331
+ const params = new URLSearchParams();
332
+ params.set('acting_as_agent_id', actingAgentId);
333
+ params.set('conversation_id', conversationId);
334
+ const { data } = await apiFetch(`/api/agent/approval/list?${params}`);
335
+ return data || [];
336
+ }
337
+
338
+ export async function resolveGoalApproval({
339
+ actingAgentId, requestId, decision, originalText, confidence, sourceMessageId,
340
+ }) {
341
+ return apiFetch('/api/agent/approval/resolve', {
342
+ method: 'POST',
343
+ body: JSON.stringify({
344
+ acting_as_agent_id: actingAgentId,
345
+ request_id: requestId,
346
+ decision,
347
+ original_text: originalText ?? null,
348
+ confidence: confidence ?? null,
349
+ source_message_id: sourceMessageId ?? null,
350
+ }),
351
+ });
352
+ }
353
+
285
354
  export async function listAgentTasks({ actingAgentId, conversationId }) {
286
355
  const params = new URLSearchParams();
287
356
  params.set('acting_as_agent_id', actingAgentId);
@@ -484,63 +553,294 @@ export async function removeAgentGroupMember({
484
553
  );
485
554
  }
486
555
 
556
+ // ── Workstreams (managed groups) ──
557
+
558
+ export async function createWorkstream({
559
+ actingAgentId, name, description, charter, memberAgentIds,
560
+ }) {
561
+ return apiFetch('/api/agent/workstreams', {
562
+ method: 'POST',
563
+ body: JSON.stringify({
564
+ acting_as_agent_id: actingAgentId,
565
+ name,
566
+ description: description ?? null,
567
+ charter: charter ?? null,
568
+ member_agent_ids: memberAgentIds || [],
569
+ }),
570
+ });
571
+ }
572
+
573
+ export async function deleteWorkstream({ actingAgentId, conversationId }) {
574
+ return apiFetch(
575
+ `/api/agent/workstreams/${encodeURIComponent(conversationId)}`,
576
+ {
577
+ method: 'DELETE',
578
+ body: JSON.stringify({ acting_as_agent_id: actingAgentId }),
579
+ },
580
+ );
581
+ }
582
+
583
+ export async function listWorkstreams({ actingAgentId }) {
584
+ const params = new URLSearchParams();
585
+ params.set('acting_as_agent_id', actingAgentId);
586
+ const { data } = await apiFetch(`/api/agent/workstreams?${params}`);
587
+ return data || [];
588
+ }
589
+
590
+ // ── Agents ──
591
+
592
+ export async function listAgentSlots({ actingAgentId }) {
593
+ const params = new URLSearchParams();
594
+ params.set('acting_as_agent_id', actingAgentId);
595
+ const { data } = await apiFetch(`/api/agent/agents?${params}`);
596
+ return data || [];
597
+ }
598
+
599
+ export async function createAgentSlot({
600
+ actingAgentId, name, runtime, description, displayName, model,
601
+ }) {
602
+ return apiFetch('/api/agent/agents', {
603
+ method: 'POST',
604
+ body: JSON.stringify({
605
+ acting_as_agent_id: actingAgentId,
606
+ name,
607
+ runtime,
608
+ description: description ?? null,
609
+ display_name: displayName ?? null,
610
+ model: model ?? null,
611
+ }),
612
+ });
613
+ }
614
+
615
+ export async function archiveAgentSlot({ actingAgentId, agentId }) {
616
+ return apiFetch(
617
+ `/api/agent/agents/${encodeURIComponent(agentId)}`,
618
+ {
619
+ method: 'DELETE',
620
+ body: JSON.stringify({ acting_as_agent_id: actingAgentId }),
621
+ },
622
+ );
623
+ }
624
+
625
+ // ── Services ──
626
+
627
+ export async function createService({
628
+ actingAgentId, name, description, contractSchema, endpointConfig,
629
+ }) {
630
+ return apiFetch('/api/agent/services', {
631
+ method: 'POST',
632
+ body: JSON.stringify({
633
+ acting_as_agent_id: actingAgentId,
634
+ name,
635
+ description: description ?? null,
636
+ contract_schema: contractSchema ?? null,
637
+ endpoint_config: endpointConfig,
638
+ }),
639
+ });
640
+ }
641
+
642
+ export async function updateService({ actingAgentId, serviceId, ...patch }) {
643
+ return apiFetch(
644
+ `/api/agent/services/${encodeURIComponent(serviceId)}`,
645
+ {
646
+ method: 'PATCH',
647
+ body: JSON.stringify({ acting_as_agent_id: actingAgentId, ...patch }),
648
+ },
649
+ );
650
+ }
651
+
652
+ export async function deleteService({ actingAgentId, serviceId }) {
653
+ return apiFetch(
654
+ `/api/agent/services/${encodeURIComponent(serviceId)}`,
655
+ {
656
+ method: 'DELETE',
657
+ body: JSON.stringify({ acting_as_agent_id: actingAgentId }),
658
+ },
659
+ );
660
+ }
661
+
662
+ export async function listServices({ actingAgentId }) {
663
+ const params = new URLSearchParams();
664
+ params.set('acting_as_agent_id', actingAgentId);
665
+ const { data } = await apiFetch(`/api/agent/services?${params}`);
666
+ return data || [];
667
+ }
668
+
669
+ export async function getServiceInfo({ actingAgentId, name }) {
670
+ const params = new URLSearchParams();
671
+ params.set('acting_as_agent_id', actingAgentId);
672
+ return apiFetch(
673
+ `/api/agent/services/${encodeURIComponent(name)}/info?${params}`,
674
+ );
675
+ }
676
+
677
+ export async function callService({ actingAgentId, name, input }) {
678
+ return apiFetch(
679
+ `/api/agent/services/${encodeURIComponent(name)}/call`,
680
+ {
681
+ method: 'POST',
682
+ body: JSON.stringify({ acting_as_agent_id: actingAgentId, input }),
683
+ },
684
+ );
685
+ }
686
+
687
+ // ── Briefings ──
688
+
689
+ export async function getBriefing({actingAgentId, briefingId}) {
690
+ const params = new URLSearchParams();
691
+ params.set('acting_as_agent_id', actingAgentId);
692
+ return apiFetch(`/api/agent/briefings/${encodeURIComponent(briefingId)}?${params}`);
693
+ }
694
+
695
+ export async function publishBriefing({actingAgentId, bodyText, attachmentAssetId, currentConversationId, responseMode}) {
696
+ const body = { acting_as_agent_id: actingAgentId };
697
+ if (bodyText != null) body.body_text = bodyText;
698
+ if (attachmentAssetId != null) body.attachment_asset_id = attachmentAssetId;
699
+ if (currentConversationId != null) body.current_conversation_id = currentConversationId;
700
+ if (responseMode != null) body.response_mode = responseMode;
701
+ return apiFetch('/api/agent/briefings', {
702
+ method: 'POST',
703
+ body: JSON.stringify(body),
704
+ });
705
+ }
706
+
707
+ // ── Credentials (slot creation + daemon sync) ──
708
+
709
+ export async function fetchCredentials() {
710
+ return apiFetch('/api/agent/credentials', { method: 'GET' });
711
+ }
712
+
713
+ export async function requestCredential({
714
+ actingAgentId, name, description, workstreamId,
715
+ }) {
716
+ return apiFetch('/api/agent/credentials', {
717
+ method: 'POST',
718
+ body: JSON.stringify({
719
+ acting_as_agent_id: actingAgentId,
720
+ name,
721
+ description: description ?? null,
722
+ workstream_id: workstreamId ?? null,
723
+ }),
724
+ });
725
+ }
726
+
727
+ // ── Workstream dashboard ──
728
+
729
+ export async function setWorkstreamDashboard({
730
+ actingAgentId, conversationId, dataJson, htmlTemplate,
731
+ }) {
732
+ const body = { acting_as_agent_id: actingAgentId };
733
+ // Distinguish "omit" from "set to null" — only include keys the caller
734
+ // explicitly passed (including null clears the field).
735
+ if (dataJson !== undefined) body.data_json = dataJson;
736
+ if (htmlTemplate !== undefined) body.html_template = htmlTemplate;
737
+ return apiFetch(
738
+ `/api/agent/workstreams/${encodeURIComponent(conversationId)}/dashboard`,
739
+ { method: 'POST', body: JSON.stringify(body) },
740
+ );
741
+ }
742
+
743
+ export async function getWorkstreamDashboard({ actingAgentId, conversationId }) {
744
+ const params = new URLSearchParams();
745
+ params.set('acting_as_agent_id', actingAgentId);
746
+ return apiFetch(
747
+ `/api/agent/workstreams/${encodeURIComponent(conversationId)}/dashboard?${params}`,
748
+ );
749
+ }
750
+
751
+ // ── Workstream charter ──
752
+
753
+ export async function getWorkstreamCharter({ actingAgentId, conversationId }) {
754
+ const params = new URLSearchParams();
755
+ params.set('acting_as_agent_id', actingAgentId);
756
+ return apiFetch(
757
+ `/api/agent/workstreams/${encodeURIComponent(conversationId)}/charter?${params}`,
758
+ );
759
+ }
760
+
761
+ export async function setWorkstreamCharter({ actingAgentId, conversationId, charter }) {
762
+ return apiFetch(
763
+ `/api/agent/workstreams/${encodeURIComponent(conversationId)}/charter`,
764
+ {
765
+ method: 'POST',
766
+ body: JSON.stringify({
767
+ acting_as_agent_id: actingAgentId,
768
+ charter: charter ?? null,
769
+ }),
770
+ },
771
+ );
772
+ }
773
+
487
774
  // ── Channel event pipe ──
488
775
 
489
776
  export async function postEvent({ agent, agent_id, runtime_host_id, session_id, cwd, runtime_version, event, required = false }) {
490
777
  const agentId = agent_id || null;
491
778
  const queueKey = agentId || `${agent}:${session_id || ''}`;
492
- const previous = agentEventQueues.get(queueKey) || Promise.resolve(null);
493
779
  const eventName = event?.worker_event_name || event?.hook_event_name || event?.event_name || 'unknown';
494
780
  const turnId = event?.turn_id || event?.reply_to_message_id || null;
495
781
  const seq = event?.event_seq ?? null;
496
782
  const deltaChars = typeof event?.delta === 'string' ? event.delta.length : null;
497
783
 
784
+ if (eventName === 'worker.message.delta') {
785
+ debugLog('events', 'delta.drop', {
786
+ agent,
787
+ agentId,
788
+ sessionId: shortId(session_id),
789
+ turnId: shortId(turnId),
790
+ deltaChars,
791
+ });
792
+ return null;
793
+ }
794
+
795
+ const postOnce = async () => {
796
+ const startedAt = Date.now();
797
+ try {
798
+ const result = await apiFetch('/api/events', {
799
+ method: 'POST',
800
+ body: JSON.stringify({ agent, agent_id: agentId, runtime_host_id, session_id, cwd, runtime_version, event }),
801
+ timeout: 5000,
802
+ });
803
+ if (required && result?.matched === false) {
804
+ throw new Error(`event was not matched (${eventName})`);
805
+ }
806
+ debugLog('events', 'post.ok', {
807
+ agent,
808
+ agentId,
809
+ sessionId: shortId(session_id),
810
+ runtimeVersion: runtime_version ?? null,
811
+ turnId: shortId(turnId),
812
+ eventName,
813
+ seq,
814
+ durationMs: Date.now() - startedAt,
815
+ });
816
+ return result;
817
+ } catch (err) {
818
+ debugError('events', 'post.failed', {
819
+ agent,
820
+ agentId,
821
+ sessionId: shortId(session_id),
822
+ runtimeVersion: runtime_version ?? null,
823
+ turnId: shortId(turnId),
824
+ eventName,
825
+ seq,
826
+ durationMs: Date.now() - startedAt,
827
+ error: err?.message || 'unknown error',
828
+ });
829
+ if (required) {
830
+ throw err;
831
+ }
832
+ return null;
833
+ }
834
+ };
835
+
836
+ if (eventName === 'worker.turn.complete' || eventName === 'worker.turn.error') {
837
+ return postOnce();
838
+ }
839
+
840
+ const previous = agentEventQueues.get(queueKey) || Promise.resolve(null);
498
841
  const queued = previous
499
842
  .catch(() => null)
500
- .then(async () => {
501
- const startedAt = Date.now();
502
- try {
503
- const result = await apiFetch('/api/events', {
504
- method: 'POST',
505
- body: JSON.stringify({ agent, agent_id: agentId, runtime_host_id, session_id, cwd, runtime_version, event }),
506
- timeout: 5000,
507
- });
508
- if (required && result?.matched === false) {
509
- throw new Error(`event was not matched (${eventName})`);
510
- }
511
- debugLog('events', 'post.ok', {
512
- agent,
513
- agentId,
514
- sessionId: shortId(session_id),
515
- runtimeVersion: runtime_version ?? null,
516
- turnId: shortId(turnId),
517
- eventName,
518
- seq,
519
- deltaChars,
520
- durationMs: Date.now() - startedAt,
521
- });
522
- return result;
523
- } catch (err) {
524
- debugError('events', 'post.failed', {
525
- agent,
526
- agentId,
527
- sessionId: shortId(session_id),
528
- runtimeVersion: runtime_version ?? null,
529
- turnId: shortId(turnId),
530
- eventName,
531
- seq,
532
- deltaChars,
533
- durationMs: Date.now() - startedAt,
534
- error: err?.message || 'unknown error',
535
- });
536
- if (required) {
537
- throw err;
538
- }
539
- // Best-effort by default. Callers that need a hard failure set
540
- // `required: true` (used for the final reply path).
541
- return null;
542
- }
543
- });
843
+ .then(postOnce);
544
844
 
545
845
  agentEventQueues.set(queueKey, queued);
546
846
  queued.finally(() => {
@@ -6,7 +6,20 @@
6
6
  * using the connector-specific env name.
7
7
  */
8
8
 
9
- import { AF_CONFIG_PATH, persistConfig, TICLAWK_CONNECTOR_API_KEY } from '../../core/config.mjs';
9
+ import { AF_CONFIG_PATH, loadPersistentConfig, persistConfig, TICLAWK_CONNECTOR_API_KEY } from '../../core/config.mjs';
10
+
11
+ export const TICLAWK_CREDENTIAL_NAMES = 'TICLAWK_CREDENTIAL_NAMES';
12
+
13
+ function isRuntimeCredentialName(value) {
14
+ return /^[A-Z][A-Z0-9_]*$/.test(String(value || '').trim());
15
+ }
16
+
17
+ function parseCredentialNames(value) {
18
+ return String(value || '')
19
+ .split(',')
20
+ .map((name) => name.trim())
21
+ .filter(isRuntimeCredentialName);
22
+ }
10
23
 
11
24
  export function persistApiCredential(apiKey) {
12
25
  if (!apiKey || !apiKey.startsWith('tk_')) return;
@@ -19,3 +32,30 @@ export function persistApiCredential(apiKey) {
19
32
  delete process.env.TICLAWK_SETUP_CODE;
20
33
  console.log(`[connect] saved ${TICLAWK_CONNECTOR_API_KEY} to ${AF_CONFIG_PATH}`);
21
34
  }
35
+
36
+ export function persistRuntimeCredentials(credentials = []) {
37
+ const current = loadPersistentConfig();
38
+ const previousNames = new Set(parseCredentialNames(current[TICLAWK_CREDENTIAL_NAMES]));
39
+ const nextNames = [];
40
+ const updates = {};
41
+
42
+ for (const credential of Array.isArray(credentials) ? credentials : []) {
43
+ const name = String(credential?.name || '').trim();
44
+ const value = typeof credential?.value === 'string' ? credential.value : '';
45
+ if (!isRuntimeCredentialName(name) || !value) continue;
46
+ updates[name] = value;
47
+ nextNames.push(name);
48
+ }
49
+
50
+ const nextNameSet = new Set(nextNames);
51
+ for (const name of previousNames) {
52
+ if (!nextNameSet.has(name)) updates[name] = '';
53
+ }
54
+ updates[TICLAWK_CREDENTIAL_NAMES] = nextNames.join(',');
55
+
56
+ persistConfig(updates);
57
+ return {
58
+ saved: nextNames.length,
59
+ removed: [...previousNames].filter((name) => !nextNameSet.has(name)).length,
60
+ };
61
+ }