ticlawk 0.1.16-dev.9 → 0.1.16
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 +15 -3
- package/bin/ticlawk.mjs +208 -21
- package/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +283 -48
- package/src/adapters/ticlawk/credentials.mjs +41 -1
- package/src/adapters/ticlawk/index.mjs +126 -121
- package/src/adapters/ticlawk/wake-client.mjs +1 -1
- package/src/cli/agent-commands.mjs +557 -18
- package/src/core/agent-cli-handlers.mjs +435 -18
- package/src/core/agent-home.mjs +81 -1
- package/src/core/argv.mjs +11 -1
- package/src/core/events/worker-events.mjs +32 -36
- package/src/core/http.mjs +119 -0
- package/src/core/runtime-contract.mjs +0 -1
- package/src/core/runtime-env.mjs +7 -0
- package/src/core/runtime-support.mjs +108 -77
- package/src/runtimes/_shared/agent-handbook.mjs +45 -0
- package/src/runtimes/_shared/brand.mjs +2 -0
- package/src/runtimes/_shared/goal-task-protocol.mjs +50 -0
- package/src/runtimes/_shared/handbook/BASICS.md +27 -0
- package/src/runtimes/_shared/handbook/COLLABORATION.md +37 -0
- package/src/runtimes/_shared/handbook/COMMUNICATION.md +55 -0
- package/src/runtimes/_shared/handbook/DM_SCOPE.md +13 -0
- package/src/runtimes/_shared/handbook/GOAL_AUTHORITY.md +46 -0
- package/src/runtimes/_shared/handbook/GOAL_TASK_CORE.md +43 -0
- package/src/runtimes/_shared/handbook/GROUP_ADMIN_SCOPE.md +21 -0
- package/src/runtimes/_shared/handbook/GROUP_MEMBER_SCOPE.md +15 -0
- package/src/runtimes/_shared/handbook/SURFACES.md +41 -0
- package/src/runtimes/_shared/handbook/TASK_WORKER.md +14 -0
- package/src/runtimes/_shared/standing-prompt.mjs +134 -278
- package/src/runtimes/_shared/wake-prompt.mjs +261 -0
- package/src/runtimes/claude-code/index.mjs +19 -46
- package/src/runtimes/claude-code/session.mjs +2 -7
- package/src/runtimes/codex/index.mjs +115 -63
- package/src/runtimes/codex/session.mjs +2 -12
- package/src/runtimes/openclaw/index.mjs +11 -24
- package/src/runtimes/opencode/index.mjs +38 -60
- package/src/runtimes/opencode/session.mjs +12 -12
- package/src/runtimes/pi/index.mjs +38 -60
- package/src/runtimes/pi/session.mjs +9 -6
- package/ticlawk.mjs +0 -30
|
@@ -1,22 +1,9 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
|
|
3
1
|
const workerEventSeq = new Map();
|
|
4
|
-
const DELTA_LOG_PREVIEW_CHARS = 48;
|
|
5
2
|
|
|
6
3
|
function shortId(value) {
|
|
7
4
|
return value ? String(value).slice(0, 8) : null;
|
|
8
5
|
}
|
|
9
6
|
|
|
10
|
-
function hashText(text) {
|
|
11
|
-
return createHash('sha1').update(String(text)).digest('hex').slice(0, 12);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function previewDelta(text) {
|
|
15
|
-
const normalized = String(text).replace(/\n/g, '\\n');
|
|
16
|
-
if (normalized.length <= DELTA_LOG_PREVIEW_CHARS) return normalized;
|
|
17
|
-
return normalized.slice(0, DELTA_LOG_PREVIEW_CHARS) + '…';
|
|
18
|
-
}
|
|
19
|
-
|
|
20
7
|
function nextWorkerEventMeta({ agent, sessionId, turnId }) {
|
|
21
8
|
const key = `${agent}:${sessionId || ''}:${turnId || 'session'}`;
|
|
22
9
|
const seq = (workerEventSeq.get(key) || 0) + 1;
|
|
@@ -37,6 +24,16 @@ export async function emitWorkerEvent({ adapter, binding, agent, sessionId, cwd
|
|
|
37
24
|
if (!sessionId || typeof adapter.emitEvent !== 'function') return;
|
|
38
25
|
const eventName = event?.worker_event_name || event?.hook_event_name || 'unknown';
|
|
39
26
|
const logicalTurnId = replyToMessageId || turnId || event?.turn_id || null;
|
|
27
|
+
if (eventName === 'worker.message.delta') {
|
|
28
|
+
logger?.debugLog?.('events', 'delta.drop', {
|
|
29
|
+
bindingId: binding.id,
|
|
30
|
+
agent,
|
|
31
|
+
sessionId: shortId(sessionId),
|
|
32
|
+
turnId: shortId(logicalTurnId),
|
|
33
|
+
chars: typeof event?.delta === 'string' ? event.delta.length : null,
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
40
37
|
const { key, seq, originTs } = nextWorkerEventMeta({ agent, sessionId, turnId: logicalTurnId });
|
|
41
38
|
const enrichedEvent = {
|
|
42
39
|
...event,
|
|
@@ -45,29 +42,14 @@ export async function emitWorkerEvent({ adapter, binding, agent, sessionId, cwd
|
|
|
45
42
|
event_seq: seq,
|
|
46
43
|
origin_ts: originTs,
|
|
47
44
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
seq,
|
|
57
|
-
chars: event.delta.length,
|
|
58
|
-
hash: enrichedEvent.delta_hash,
|
|
59
|
-
preview: previewDelta(event.delta),
|
|
60
|
-
});
|
|
61
|
-
} else {
|
|
62
|
-
logger?.debugLog?.('events', 'event.recv', {
|
|
63
|
-
bindingId: binding.id,
|
|
64
|
-
agent,
|
|
65
|
-
sessionId: shortId(sessionId),
|
|
66
|
-
turnId: shortId(logicalTurnId),
|
|
67
|
-
seq,
|
|
68
|
-
eventName,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
45
|
+
logger?.debugLog?.('events', 'event.recv', {
|
|
46
|
+
bindingId: binding.id,
|
|
47
|
+
agent,
|
|
48
|
+
sessionId: shortId(sessionId),
|
|
49
|
+
turnId: shortId(logicalTurnId),
|
|
50
|
+
seq,
|
|
51
|
+
eventName,
|
|
52
|
+
});
|
|
71
53
|
await adapter.emitEvent(binding, {
|
|
72
54
|
agent,
|
|
73
55
|
sessionId,
|
|
@@ -78,3 +60,17 @@ export async function emitWorkerEvent({ adapter, binding, agent, sessionId, cwd
|
|
|
78
60
|
clearWorkerEventMeta(key);
|
|
79
61
|
}
|
|
80
62
|
}
|
|
63
|
+
|
|
64
|
+
export function emitWorkerEventBestEffort(args) {
|
|
65
|
+
void emitWorkerEvent(args).catch((err) => {
|
|
66
|
+
const event = args?.event || {};
|
|
67
|
+
const eventName = event.worker_event_name || event.hook_event_name || 'unknown';
|
|
68
|
+
args?.logger?.debugError?.('events', 'event.best-effort-failed', {
|
|
69
|
+
bindingId: args?.binding?.id || null,
|
|
70
|
+
agent: args?.agent || null,
|
|
71
|
+
sessionId: shortId(args?.sessionId),
|
|
72
|
+
eventName,
|
|
73
|
+
error: err?.message || 'unknown error',
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
package/src/core/http.mjs
CHANGED
|
@@ -3,6 +3,25 @@ import {
|
|
|
3
3
|
handleAttachmentUpload,
|
|
4
4
|
handleAttachmentView,
|
|
5
5
|
handleGroupCreate,
|
|
6
|
+
handleWorkstreamCharterGet,
|
|
7
|
+
handleWorkstreamCharterSet,
|
|
8
|
+
handleWorkstreamCreate,
|
|
9
|
+
handleWorkstreamDelete,
|
|
10
|
+
handleWorkstreamList,
|
|
11
|
+
handleAgentList,
|
|
12
|
+
handleAgentCreate,
|
|
13
|
+
handleAgentDelete,
|
|
14
|
+
handleWorkstreamDashboardSet,
|
|
15
|
+
handleWorkstreamDashboardGet,
|
|
16
|
+
handleCredentialRequest,
|
|
17
|
+
handleBriefingPublish,
|
|
18
|
+
handleBriefingGet,
|
|
19
|
+
handleServiceCreate,
|
|
20
|
+
handleServiceUpdate,
|
|
21
|
+
handleServiceDelete,
|
|
22
|
+
handleServiceList,
|
|
23
|
+
handleServiceInfo,
|
|
24
|
+
handleServiceCall,
|
|
6
25
|
handleGroupMembers,
|
|
7
26
|
handleGroupMembersAdd,
|
|
8
27
|
handleGroupMembersRemove,
|
|
@@ -223,6 +242,106 @@ export function startLocalHttpServer({ port, adapter, ctx }) {
|
|
|
223
242
|
const r = await handleServerInfo(req, parseQuery(req.url || ''), cliCtx);
|
|
224
243
|
return writeJson(res, r.status, r.body);
|
|
225
244
|
}
|
|
245
|
+
if (urlNoQuery === '/agent/workstream/charter/get' && method === 'GET') {
|
|
246
|
+
const r = await handleWorkstreamCharterGet(req, parseQuery(req.url || ''), cliCtx);
|
|
247
|
+
return writeJson(res, r.status, r.body);
|
|
248
|
+
}
|
|
249
|
+
if (urlNoQuery === '/agent/workstream/charter/set' && method === 'POST') {
|
|
250
|
+
const body = await readJsonBody(req);
|
|
251
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
252
|
+
const r = await handleWorkstreamCharterSet(req, body, cliCtx);
|
|
253
|
+
return writeJson(res, r.status, r.body);
|
|
254
|
+
}
|
|
255
|
+
if (urlNoQuery === '/agent/workstream/create' && method === 'POST') {
|
|
256
|
+
const body = await readJsonBody(req);
|
|
257
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
258
|
+
const r = await handleWorkstreamCreate(req, body, cliCtx);
|
|
259
|
+
return writeJson(res, r.status, r.body);
|
|
260
|
+
}
|
|
261
|
+
if (urlNoQuery === '/agent/workstream/delete' && method === 'POST') {
|
|
262
|
+
const body = await readJsonBody(req);
|
|
263
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
264
|
+
const r = await handleWorkstreamDelete(req, body, cliCtx);
|
|
265
|
+
return writeJson(res, r.status, r.body);
|
|
266
|
+
}
|
|
267
|
+
if (urlNoQuery === '/agent/workstream/list' && method === 'GET') {
|
|
268
|
+
const r = await handleWorkstreamList(req, parseQuery(req.url || ''), cliCtx);
|
|
269
|
+
return writeJson(res, r.status, r.body);
|
|
270
|
+
}
|
|
271
|
+
if (urlNoQuery === '/agent/agent/list' && method === 'GET') {
|
|
272
|
+
const r = await handleAgentList(req, parseQuery(req.url || ''), cliCtx);
|
|
273
|
+
return writeJson(res, r.status, r.body);
|
|
274
|
+
}
|
|
275
|
+
if (urlNoQuery === '/agent/agent/create' && method === 'POST') {
|
|
276
|
+
const body = await readJsonBody(req);
|
|
277
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
278
|
+
const r = await handleAgentCreate(req, body, cliCtx);
|
|
279
|
+
return writeJson(res, r.status, r.body);
|
|
280
|
+
}
|
|
281
|
+
if (urlNoQuery === '/agent/agent/delete' && method === 'POST') {
|
|
282
|
+
const body = await readJsonBody(req);
|
|
283
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
284
|
+
const r = await handleAgentDelete(req, body, cliCtx);
|
|
285
|
+
return writeJson(res, r.status, r.body);
|
|
286
|
+
}
|
|
287
|
+
if (urlNoQuery === '/agent/dashboard/set' && method === 'POST') {
|
|
288
|
+
const body = await readJsonBody(req);
|
|
289
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
290
|
+
const r = await handleWorkstreamDashboardSet(req, body, cliCtx);
|
|
291
|
+
return writeJson(res, r.status, r.body);
|
|
292
|
+
}
|
|
293
|
+
if (urlNoQuery === '/agent/dashboard/get' && method === 'GET') {
|
|
294
|
+
const r = await handleWorkstreamDashboardGet(req, parseQuery(req.url || ''), cliCtx);
|
|
295
|
+
return writeJson(res, r.status, r.body);
|
|
296
|
+
}
|
|
297
|
+
if (urlNoQuery === '/agent/briefing/publish' && method === 'POST') {
|
|
298
|
+
const body = await readJsonBody(req);
|
|
299
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
300
|
+
const r = await handleBriefingPublish(req, body, cliCtx);
|
|
301
|
+
return writeJson(res, r.status, r.body);
|
|
302
|
+
}
|
|
303
|
+
if (urlNoQuery === '/agent/briefing/get' && method === 'GET') {
|
|
304
|
+
const r = await handleBriefingGet(req, parseQuery(req.url || ''), cliCtx);
|
|
305
|
+
return writeJson(res, r.status, r.body);
|
|
306
|
+
}
|
|
307
|
+
if (urlNoQuery === '/agent/credential/request' && method === 'POST') {
|
|
308
|
+
const body = await readJsonBody(req);
|
|
309
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
310
|
+
const r = await handleCredentialRequest(req, body, cliCtx);
|
|
311
|
+
return writeJson(res, r.status, r.body);
|
|
312
|
+
}
|
|
313
|
+
if (urlNoQuery === '/agent/service/create' && method === 'POST') {
|
|
314
|
+
const body = await readJsonBody(req);
|
|
315
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
316
|
+
const r = await handleServiceCreate(req, body, cliCtx);
|
|
317
|
+
return writeJson(res, r.status, r.body);
|
|
318
|
+
}
|
|
319
|
+
if (urlNoQuery === '/agent/service/update' && method === 'POST') {
|
|
320
|
+
const body = await readJsonBody(req);
|
|
321
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
322
|
+
const r = await handleServiceUpdate(req, body, cliCtx);
|
|
323
|
+
return writeJson(res, r.status, r.body);
|
|
324
|
+
}
|
|
325
|
+
if (urlNoQuery === '/agent/service/delete' && method === 'POST') {
|
|
326
|
+
const body = await readJsonBody(req);
|
|
327
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
328
|
+
const r = await handleServiceDelete(req, body, cliCtx);
|
|
329
|
+
return writeJson(res, r.status, r.body);
|
|
330
|
+
}
|
|
331
|
+
if (urlNoQuery === '/agent/service/list' && method === 'GET') {
|
|
332
|
+
const r = await handleServiceList(req, parseQuery(req.url || ''), cliCtx);
|
|
333
|
+
return writeJson(res, r.status, r.body);
|
|
334
|
+
}
|
|
335
|
+
if (urlNoQuery === '/agent/service/info' && method === 'GET') {
|
|
336
|
+
const r = await handleServiceInfo(req, parseQuery(req.url || ''), cliCtx);
|
|
337
|
+
return writeJson(res, r.status, r.body);
|
|
338
|
+
}
|
|
339
|
+
if (urlNoQuery === '/agent/service/call' && method === 'POST') {
|
|
340
|
+
const body = await readJsonBody(req);
|
|
341
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
342
|
+
const r = await handleServiceCall(req, body, cliCtx);
|
|
343
|
+
return writeJson(res, r.status, r.body);
|
|
344
|
+
}
|
|
226
345
|
|
|
227
346
|
writeJson(res, 404, { error: 'not found' });
|
|
228
347
|
} catch (err) {
|
|
@@ -55,7 +55,6 @@ import { normalizeServiceType } from './runtime-registry.mjs';
|
|
|
55
55
|
* @property {(inbound: any, ctx: RuntimeDeliveryContext) => Promise<boolean>} deliverTurn
|
|
56
56
|
* @property {(binding: any, ctx: { adapter: any, logger: any }) => (Promise<void>|void)} [onBindingUpdated]
|
|
57
57
|
* @property {(ctx: { adapter: any, getBinding: (bindingId: string) => any }) => (Promise<number>|number)} [recoverInFlight]
|
|
58
|
-
* @property {(binding: any, ctx: { adapter: any, logger: any }) => (Promise<number>|number)} [reconcileAfterRestart]
|
|
59
58
|
*/
|
|
60
59
|
|
|
61
60
|
/**
|
package/src/core/runtime-env.mjs
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
const STRIPPED_KEYS = new Set([
|
|
16
16
|
'TICLAWK_CONNECTOR_API_KEY',
|
|
17
17
|
'TICLAWK_CONNECTOR_WS_URL',
|
|
18
|
+
'TICLAWK_CREDENTIAL_NAMES',
|
|
18
19
|
'TICLAWK_SETUP_CODE',
|
|
19
20
|
]);
|
|
20
21
|
|
|
@@ -35,11 +36,17 @@ export function buildAgentRuntimeEnv({
|
|
|
35
36
|
sessionId,
|
|
36
37
|
hostId,
|
|
37
38
|
daemonUrl,
|
|
39
|
+
conversationId,
|
|
40
|
+
messageId,
|
|
41
|
+
target,
|
|
38
42
|
} = {}) {
|
|
39
43
|
const out = {};
|
|
40
44
|
if (agentId) out.TICLAWK_RUNTIME_AGENT_ID = String(agentId);
|
|
41
45
|
if (sessionId) out.TICLAWK_RUNTIME_SESSION_ID = String(sessionId);
|
|
42
46
|
if (hostId) out.TICLAWK_RUNTIME_HOST_ID = String(hostId);
|
|
47
|
+
if (conversationId) out.TICLAWK_RUNTIME_CONVERSATION_ID = String(conversationId);
|
|
48
|
+
if (messageId) out.TICLAWK_RUNTIME_MESSAGE_ID = String(messageId);
|
|
49
|
+
if (target) out.TICLAWK_RUNTIME_TARGET = String(target);
|
|
43
50
|
out.TICLAWK_RUNTIME_DAEMON_URL = daemonUrl || 'http://127.0.0.1:8741';
|
|
44
51
|
return out;
|
|
45
52
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getStreamingMode } from './config.mjs';
|
|
2
2
|
import { getAgentHome } from './agent-home.mjs';
|
|
3
|
+
import { debugError } from './logger.mjs';
|
|
3
4
|
const ERROR_MAX_CHARS = 500;
|
|
4
|
-
const
|
|
5
|
-
const DEFAULT_DELTA_FLUSH_CHARS = 64;
|
|
5
|
+
const MAX_SCOPED_RUNTIME_SESSIONS = 50;
|
|
6
6
|
|
|
7
7
|
function truncateError(text) {
|
|
8
8
|
if (!text) return null;
|
|
@@ -74,12 +74,21 @@ export async function reportSubprocessFailure({ adapter, binding, inbound, runti
|
|
|
74
74
|
// skips fan-out to member-role agents, breaking what would otherwise
|
|
75
75
|
// be a failure→fan-out→failure cascade when several agents share a
|
|
76
76
|
// broken runtime.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
try {
|
|
78
|
+
await adapter.postAgentReply(binding, {
|
|
79
|
+
conversationId: inbound?.conversationId || null,
|
|
80
|
+
text,
|
|
81
|
+
replyToMessageId: inbound?.messageId || null,
|
|
82
|
+
visibility: 'admin',
|
|
83
|
+
});
|
|
84
|
+
} catch (err) {
|
|
85
|
+
debugError('runtime', 'failure-notice.failed', {
|
|
86
|
+
agentId: binding?.id || null,
|
|
87
|
+
runtimeName,
|
|
88
|
+
messageId: inbound?.messageId || null,
|
|
89
|
+
error: err?.message || 'unknown error',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
export function terminalRuntimeFailure(reason = 'runtime failure') {
|
|
@@ -105,80 +114,102 @@ export async function updateBindingRuntimeMeta(ctx, binding, runtimeMetaPatch, e
|
|
|
105
114
|
});
|
|
106
115
|
}
|
|
107
116
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
if (!
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
117
|
+
function readScopedSessionKey(inbound = {}) {
|
|
118
|
+
const conversationId = String(inbound?.conversationId || '').trim();
|
|
119
|
+
if (!conversationId) return '';
|
|
120
|
+
const threadRoot = String(inbound?.raw?.thread_root_message_id || '').trim();
|
|
121
|
+
return threadRoot ? `${conversationId}:thread:${threadRoot}` : conversationId;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeScopedRuntimeSessions(value) {
|
|
125
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
126
|
+
const out = {};
|
|
127
|
+
for (const [key, session] of Object.entries(value)) {
|
|
128
|
+
if (!key || !session || typeof session !== 'object' || Array.isArray(session)) continue;
|
|
129
|
+
const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
|
|
130
|
+
if (!sessionId) continue;
|
|
131
|
+
out[key] = {
|
|
132
|
+
sessionId,
|
|
133
|
+
path: typeof session.path === 'string' ? session.path : null,
|
|
134
|
+
lastRotatedAt: typeof session.lastRotatedAt === 'string' ? session.lastRotatedAt : null,
|
|
135
|
+
updatedAt: typeof session.updatedAt === 'string' ? session.updatedAt : null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
123
140
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
141
|
+
function pruneScopedRuntimeSessions(sessions) {
|
|
142
|
+
const entries = Object.entries(sessions);
|
|
143
|
+
if (entries.length <= MAX_SCOPED_RUNTIME_SESSIONS) return sessions;
|
|
144
|
+
return Object.fromEntries(entries
|
|
145
|
+
.sort(([, a], [, b]) => {
|
|
146
|
+
const aTs = Date.parse(a?.updatedAt || a?.lastRotatedAt || '') || 0;
|
|
147
|
+
const bTs = Date.parse(b?.updatedAt || b?.lastRotatedAt || '') || 0;
|
|
148
|
+
return bTs - aTs;
|
|
149
|
+
})
|
|
150
|
+
.slice(0, MAX_SCOPED_RUNTIME_SESSIONS));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function resolveRuntimeSessionScope(meta = {}, inbound = {}) {
|
|
154
|
+
const key = readScopedSessionKey(inbound);
|
|
155
|
+
if (!key) {
|
|
156
|
+
return {
|
|
157
|
+
key: '',
|
|
158
|
+
sessions: {},
|
|
159
|
+
sessionId: meta.sessionId || null,
|
|
160
|
+
path: meta.path || null,
|
|
161
|
+
lastRotatedAt: meta.lastRotatedAt || null,
|
|
162
|
+
shouldRotate: !meta.sessionId || Boolean(meta.rotatePending),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const sessions = meta.rotatePending ? {} : normalizeScopedRuntimeSessions(meta.conversationSessions);
|
|
167
|
+
const scoped = sessions[key] || {};
|
|
168
|
+
return {
|
|
169
|
+
key,
|
|
170
|
+
sessions,
|
|
171
|
+
sessionId: scoped.sessionId || null,
|
|
172
|
+
path: scoped.path || null,
|
|
173
|
+
lastRotatedAt: scoped.lastRotatedAt || null,
|
|
174
|
+
shouldRotate: !scoped.sessionId || Boolean(meta.rotatePending),
|
|
135
175
|
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function buildRuntimeSessionMetaPatch(meta = {}, scope = {}, result = {}) {
|
|
179
|
+
const now = new Date().toISOString();
|
|
180
|
+
const scoped = Boolean(scope.key);
|
|
181
|
+
const sessionId = result?.sessionId || scope.sessionId || (scoped ? null : meta.sessionId || null);
|
|
182
|
+
const path = result?.path || scope.path || (scoped ? null : meta.path || null);
|
|
183
|
+
const lastRotatedAt = scope.shouldRotate
|
|
184
|
+
? now
|
|
185
|
+
: (scope.lastRotatedAt || meta.lastRotatedAt || now);
|
|
186
|
+
|
|
187
|
+
if (!scoped) {
|
|
188
|
+
return {
|
|
189
|
+
sessionId,
|
|
190
|
+
...(path !== undefined ? { path } : {}),
|
|
191
|
+
rotatePending: false,
|
|
192
|
+
lastRotatedAt,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
136
195
|
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
timer = setTimeout(() => {
|
|
140
|
-
timer = null;
|
|
141
|
-
void startFlush();
|
|
142
|
-
}, flushMs);
|
|
143
|
-
timer.unref?.();
|
|
196
|
+
const sessions = {
|
|
197
|
+
...(scope.sessions || {}),
|
|
144
198
|
};
|
|
199
|
+
if (sessionId) {
|
|
200
|
+
sessions[scope.key] = {
|
|
201
|
+
sessionId,
|
|
202
|
+
path,
|
|
203
|
+
lastRotatedAt,
|
|
204
|
+
updatedAt: now,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
145
207
|
|
|
146
208
|
return {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
sessionId: meta.sessionId || null,
|
|
153
|
-
turnId: meta.turnId || null,
|
|
154
|
-
cwd: meta.cwd || '',
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
const sameContext = !pendingMeta
|
|
158
|
-
|| (
|
|
159
|
-
pendingMeta.sessionId === nextMeta.sessionId
|
|
160
|
-
&& pendingMeta.turnId === nextMeta.turnId
|
|
161
|
-
&& pendingMeta.cwd === nextMeta.cwd
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
if (!sameContext && buffer) {
|
|
165
|
-
void startFlush();
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
pendingMeta = nextMeta;
|
|
169
|
-
buffer += normalized;
|
|
170
|
-
|
|
171
|
-
if (buffer.length >= flushChars) {
|
|
172
|
-
void startFlush();
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
scheduleFlush();
|
|
177
|
-
},
|
|
178
|
-
|
|
179
|
-
async flush() {
|
|
180
|
-
clearTimer();
|
|
181
|
-
await startFlush();
|
|
182
|
-
},
|
|
209
|
+
sessionId,
|
|
210
|
+
path,
|
|
211
|
+
conversationSessions: pruneScopedRuntimeSessions(sessions),
|
|
212
|
+
rotatePending: false,
|
|
213
|
+
lastRotatedAt,
|
|
183
214
|
};
|
|
184
215
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
export const HANDBOOK_BASICS_FILE = 'BASICS.md';
|
|
6
|
+
|
|
7
|
+
export const HANDBOOK_FILE_NAMES = Object.freeze([
|
|
8
|
+
HANDBOOK_BASICS_FILE,
|
|
9
|
+
'COMMUNICATION.md',
|
|
10
|
+
'COLLABORATION.md',
|
|
11
|
+
'GOAL_TASK_CORE.md',
|
|
12
|
+
'GOAL_AUTHORITY.md',
|
|
13
|
+
'TASK_WORKER.md',
|
|
14
|
+
'DM_SCOPE.md',
|
|
15
|
+
'GROUP_ADMIN_SCOPE.md',
|
|
16
|
+
'GROUP_MEMBER_SCOPE.md',
|
|
17
|
+
'SURFACES.md',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export const LEGACY_HANDBOOK_FILE_NAMES = Object.freeze([
|
|
21
|
+
'TICLAWK.md',
|
|
22
|
+
'TICLAWK_CLI.md',
|
|
23
|
+
'TICLAWK_COLLABORATION.md',
|
|
24
|
+
'TICLAWK_GOAL_TASK_PROTOCOL.md',
|
|
25
|
+
'TICLAWK_GOAL_TASK_CORE.md',
|
|
26
|
+
'TICLAWK_GOAL_AUTHORITY.md',
|
|
27
|
+
'TICLAWK_TASK_WORKER.md',
|
|
28
|
+
'TICLAWK_DM_SCOPE.md',
|
|
29
|
+
'TICLAWK_GROUP_ADMIN_SCOPE.md',
|
|
30
|
+
'TICLAWK_GROUP_MEMBER_SCOPE.md',
|
|
31
|
+
'TICLAWK_SURFACES.md',
|
|
32
|
+
'TICLAWK_WORKSPACE.md',
|
|
33
|
+
'WORKSPACE.md',
|
|
34
|
+
'GOAL_TASK_PROTOCOL.md',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
const HANDBOOK_DIR = join(MODULE_DIR, 'handbook');
|
|
39
|
+
|
|
40
|
+
export function buildAgentHandbookFiles() {
|
|
41
|
+
return HANDBOOK_FILE_NAMES.map((name) => ({
|
|
42
|
+
name,
|
|
43
|
+
content: readFileSync(join(HANDBOOK_DIR, name), 'utf8').trim(),
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime goal/task branch selection.
|
|
3
|
+
*
|
|
4
|
+
* Prompt text lives in the managed handbook files. This module only derives
|
|
5
|
+
* the current conversation scope, role, and goal-authority branch for runtime
|
|
6
|
+
* prompt selection and cache keys.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function getInboundRaw(ctx = {}) {
|
|
10
|
+
return ctx?.inbound?.raw && typeof ctx.inbound.raw === 'object'
|
|
11
|
+
? ctx.inbound.raw
|
|
12
|
+
: {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function inferScope(ctx = {}) {
|
|
16
|
+
const raw = getInboundRaw(ctx);
|
|
17
|
+
const conversationType = String(raw.conversation_type || '').trim();
|
|
18
|
+
if (conversationType === 'group' || conversationType === 'thread') return 'group';
|
|
19
|
+
return 'dm';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getRecipientConversationRole(ctx = {}) {
|
|
23
|
+
const raw = getInboundRaw(ctx);
|
|
24
|
+
return String(raw.recipient_conversation_role || raw.recipient_role || '').trim().toLowerCase() || 'member';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function hasConversationAdminRole(ctx = {}) {
|
|
28
|
+
const raw = getInboundRaw(ctx);
|
|
29
|
+
if (raw.recipient_is_conversation_admin === true) return true;
|
|
30
|
+
const role = getRecipientConversationRole(ctx);
|
|
31
|
+
return role === 'admin' || role === 'owner';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasGoalAuthority(ctx = {}) {
|
|
35
|
+
const scope = inferScope(ctx);
|
|
36
|
+
if (scope === 'dm') return true;
|
|
37
|
+
return hasConversationAdminRole(ctx);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function selectGoalTaskProtocolOverlays(ctx = {}) {
|
|
41
|
+
const scope = inferScope(ctx);
|
|
42
|
+
const recipientRole = getRecipientConversationRole(ctx);
|
|
43
|
+
const goalAuthority = hasGoalAuthority(ctx);
|
|
44
|
+
return { scope, recipientRole, goalAuthority };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getGoalTaskProtocolOverlayKey(ctx = {}) {
|
|
48
|
+
const overlays = selectGoalTaskProtocolOverlays(ctx);
|
|
49
|
+
return `${overlays.scope}:${overlays.recipientRole}:${overlays.goalAuthority ? 'goal' : 'task'}`;
|
|
50
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Agent Basics
|
|
2
|
+
|
|
3
|
+
DO NOT EDIT.
|
|
4
|
+
|
|
5
|
+
## Workspace
|
|
6
|
+
|
|
7
|
+
- Your cwd is a persistent, agent-owned workspace.
|
|
8
|
+
- Use it for memory, notes, artifacts, code checkouts, and task files.
|
|
9
|
+
- `MEMORY.md` is the recovery entry point. Read it before acting.
|
|
10
|
+
- Use `notes/<topic>.md` for long-lived detail and link those notes from `MEMORY.md`.
|
|
11
|
+
- Do not store secrets, chat transcripts, or routine progress logs in memory.
|
|
12
|
+
- When working in a repository, choose the specific project directory or worktree before running git or package commands.
|
|
13
|
+
|
|
14
|
+
## Work Contract
|
|
15
|
+
|
|
16
|
+
- Normal assistant output is private activity text inside your workspace. It is not sent to users, groups, or other agents.
|
|
17
|
+
- External communication goes through the `ticlawk` CLI.
|
|
18
|
+
- Complete the requested work before stopping. A progress update is allowed, but it is not completion.
|
|
19
|
+
- Read only the local files needed for the current turn.
|
|
20
|
+
|
|
21
|
+
## Memory Updates
|
|
22
|
+
|
|
23
|
+
Update `MEMORY.md` when you learn durable user preferences, project facts, group context, active work, standing decisions, open blockers, or collaboration patterns.
|
|
24
|
+
|
|
25
|
+
Keep memory concise. Link detailed notes instead of turning `MEMORY.md` into a transcript.
|
|
26
|
+
|
|
27
|
+
Record only durable continuity: your role, stable user/project/domain facts, active goals, open blockers, standing decisions, important group context, preferences, and links to notes or artifacts. Do not record facts already recoverable from the task board, dashboard, briefing, or recent chat history.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Collaboration
|
|
2
|
+
|
|
3
|
+
DO NOT EDIT.
|
|
4
|
+
|
|
5
|
+
## DMs
|
|
6
|
+
|
|
7
|
+
- In a DM, handle the direct ask and own follow-through until it is answered, blocked, transferred, or complete.
|
|
8
|
+
- If the DM refers to group or task work, route it back to the relevant group or task while still answering the user clearly.
|
|
9
|
+
|
|
10
|
+
## Groups
|
|
11
|
+
|
|
12
|
+
- Direct mentions, DMs, assignments, and manual reminders normally require action.
|
|
13
|
+
- Ambient group messages are visible context, not automatic work. Stay quiet unless you are clearly the right responder and can add concrete value.
|
|
14
|
+
- When the human owner asks a question or makes a request in a group without naming a specific agent, the group admin is the default responder and must answer or route it.
|
|
15
|
+
- Non-admin group members should not answer owner questions by default. Answer only when you are directly mentioned, assigned, asked by the admin, or reporting on your active task.
|
|
16
|
+
- Write like a teammate coordinating work, not like a protocol trace.
|
|
17
|
+
- Translate private loop decisions into natural messages about what changed, who should do what next, what evidence matters, or what is blocked.
|
|
18
|
+
- Do not echo someone else's completion report or PR summary. The person doing the work should report on it.
|
|
19
|
+
|
|
20
|
+
## Assigning Work
|
|
21
|
+
|
|
22
|
+
When assigning work, send a compact instruction with:
|
|
23
|
+
|
|
24
|
+
- desired outcome
|
|
25
|
+
- important constraints
|
|
26
|
+
- expected evidence
|
|
27
|
+
- where to report back
|
|
28
|
+
|
|
29
|
+
Avoid exposing internal checklists unless the group explicitly asks for process detail.
|
|
30
|
+
|
|
31
|
+
## Blockers
|
|
32
|
+
|
|
33
|
+
If you own a concrete blocker before stopping, follow the goal/task protocol for owner intervention when applicable. Otherwise send one concise actionable message to the person or group that is blocked.
|
|
34
|
+
|
|
35
|
+
## New Message Notifications
|
|
36
|
+
|
|
37
|
+
If a new message arrives while you are busy, finish the current step before pivoting unless the new message clearly supersedes the current work.
|