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.
- package/README.md +17 -3
- package/bin/ticlawk.mjs +255 -21
- package/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +350 -50
- package/src/adapters/ticlawk/credentials.mjs +41 -1
- package/src/adapters/ticlawk/index.mjs +248 -130
- package/src/adapters/ticlawk/wake-client.mjs +1 -1
- package/src/cli/agent-commands.mjs +715 -18
- package/src/core/agent-cli-handlers.mjs +556 -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 +152 -0
- package/src/core/runtime-contract.mjs +0 -1
- package/src/core/runtime-env.mjs +7 -0
- package/src/core/runtime-support.mjs +130 -78
- package/src/runtimes/_shared/agent-handbook.mjs +45 -0
- package/src/runtimes/_shared/brand.mjs +2 -0
- package/src/runtimes/_shared/goal-step-prompt.mjs +98 -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 +47 -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 +124 -279
- package/src/runtimes/_shared/wake-prompt.mjs +268 -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
package/src/core/agent-home.mjs
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* lives in cwd, agent reads it via `cat MEMORY.md`.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { AF_HOME } from './config.mjs';
|
|
13
|
+
import { buildAgentHandbookFiles, LEGACY_HANDBOOK_FILE_NAMES } from '../runtimes/_shared/agent-handbook.mjs';
|
|
13
14
|
|
|
14
15
|
export const AF_AGENTS_DIR = join(AF_HOME, 'agents');
|
|
15
16
|
|
|
@@ -37,9 +38,88 @@ export function ensureAgentHome(agentId, { displayName } = {}) {
|
|
|
37
38
|
if (!existsSync(memoryPath)) {
|
|
38
39
|
writeFileSync(memoryPath, buildInitialMemoryMd({ displayName, home }), 'utf8');
|
|
39
40
|
}
|
|
41
|
+
writeManagedHandbookFiles(home);
|
|
42
|
+
ensureSkillSymlinks(home);
|
|
40
43
|
return home;
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
function writeManagedHandbookFiles(home) {
|
|
47
|
+
removeLegacyManagedHandbookFiles(home);
|
|
48
|
+
for (const { name, content } of buildAgentHandbookFiles()) {
|
|
49
|
+
const path = join(home, name);
|
|
50
|
+
try {
|
|
51
|
+
let stat = null;
|
|
52
|
+
try { stat = lstatSync(path); } catch { /* not present */ }
|
|
53
|
+
if (stat && !stat.isFile()) continue;
|
|
54
|
+
const next = `${content.trim()}\n`;
|
|
55
|
+
if (stat) {
|
|
56
|
+
const current = readFileSync(path, 'utf8');
|
|
57
|
+
if (current === next) continue;
|
|
58
|
+
}
|
|
59
|
+
writeFileSync(path, next, { encoding: 'utf8', mode: 0o600 });
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.warn(`[agent-home] failed to write ${path}: ${err?.message || err}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function removeLegacyManagedHandbookFiles(home) {
|
|
67
|
+
for (const name of LEGACY_HANDBOOK_FILE_NAMES) {
|
|
68
|
+
const path = join(home, name);
|
|
69
|
+
try {
|
|
70
|
+
const stat = lstatSync(path);
|
|
71
|
+
if (!stat.isFile()) continue;
|
|
72
|
+
unlinkSync(path);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (err?.code === 'ENOENT') continue;
|
|
75
|
+
console.warn(`[agent-home] failed to remove legacy handbook ${path}: ${err?.message || err}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Cross-runtime skill discovery: every runtime (Claude Code, Codex,
|
|
82
|
+
* opencode, pi, openclaw) auto-discovers SKILL.md under at least one of
|
|
83
|
+
* .claude/skills/, .codex/skills/, .opencode/skills/, .pi/skills/, or
|
|
84
|
+
* .agents/skills/. We pin everything to a single source-of-truth
|
|
85
|
+
* directory (.agents/skills/) and symlink the rest so a skill written
|
|
86
|
+
* once is visible to every runtime without sync machinery.
|
|
87
|
+
*
|
|
88
|
+
* .pi and .opencode skip the symlink — both natively scan
|
|
89
|
+
* .agents/skills/ in addition to their own folder.
|
|
90
|
+
*
|
|
91
|
+
* No migration / no recovery: if the target path already exists as a
|
|
92
|
+
* real dir or wrong-target symlink, we leave it alone. The user (or a
|
|
93
|
+
* future agent) resolves it manually. See cos_impl.md §一.不为错误兜底.
|
|
94
|
+
*/
|
|
95
|
+
function ensureSkillSymlinks(home) {
|
|
96
|
+
const realRoot = join(home, '.agents', 'skills');
|
|
97
|
+
mkdirSync(realRoot, { recursive: true });
|
|
98
|
+
|
|
99
|
+
const links = [
|
|
100
|
+
{ dir: join(home, '.claude'), name: 'skills', target: '../.agents/skills' },
|
|
101
|
+
{ dir: join(home, '.codex'), name: 'skills', target: '../.agents/skills' },
|
|
102
|
+
// openclaw expects skills/ at the home root
|
|
103
|
+
{ dir: home, name: 'skills', target: '.agents/skills' },
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
for (const { dir, name, target } of links) {
|
|
107
|
+
mkdirSync(dir, { recursive: true });
|
|
108
|
+
const linkPath = join(dir, name);
|
|
109
|
+
let stat = null;
|
|
110
|
+
try { stat = lstatSync(linkPath); } catch { /* not present */ }
|
|
111
|
+
if (stat) continue; // path already exists — do not touch
|
|
112
|
+
try {
|
|
113
|
+
symlinkSync(target, linkPath, 'dir');
|
|
114
|
+
} catch (err) {
|
|
115
|
+
// Best-effort: an EEXIST race or platform that disallows symlinks
|
|
116
|
+
// gets logged but doesn't fail spawn. Skills just won't be visible
|
|
117
|
+
// for that runtime via this folder.
|
|
118
|
+
console.warn(`[agent-home] failed to symlink ${linkPath} -> ${target}: ${err?.message || err}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
43
123
|
function buildInitialMemoryMd({ displayName, home }) {
|
|
44
124
|
const lines = [
|
|
45
125
|
`# ${displayName || 'Agent'}`,
|
package/src/core/argv.mjs
CHANGED
|
@@ -29,7 +29,17 @@ export function parseOptionArgs(argv = []) {
|
|
|
29
29
|
const value = inlineValue !== undefined
|
|
30
30
|
? inlineValue
|
|
31
31
|
: argv[i + 1] && !argv[i + 1].startsWith('-') ? argv[++i] : true;
|
|
32
|
-
|
|
32
|
+
// Repeated flags collect into an array so `--attach a --attach b`
|
|
33
|
+
// surfaces as ['a','b'] to callers that expect repeatable input
|
|
34
|
+
// (--attach on message send, --member on group create, etc).
|
|
35
|
+
// First occurrence stays scalar so single-value callers don't
|
|
36
|
+
// have to learn array-or-string.
|
|
37
|
+
if (Object.prototype.hasOwnProperty.call(args, rawKey)) {
|
|
38
|
+
const existing = args[rawKey];
|
|
39
|
+
args[rawKey] = Array.isArray(existing) ? [...existing, value] : [existing, value];
|
|
40
|
+
} else {
|
|
41
|
+
args[rawKey] = value;
|
|
42
|
+
}
|
|
33
43
|
continue;
|
|
34
44
|
}
|
|
35
45
|
args._.push(arg);
|
|
@@ -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,
|
|
@@ -20,6 +39,11 @@ import {
|
|
|
20
39
|
handleReminderSchedule,
|
|
21
40
|
handleReminderSnooze,
|
|
22
41
|
handleReminderUpdate,
|
|
42
|
+
handleGoalChanged,
|
|
43
|
+
handleGoalReport,
|
|
44
|
+
handleApprovalRequest,
|
|
45
|
+
handleApprovalResolve,
|
|
46
|
+
handleApprovalList,
|
|
23
47
|
handleServerInfo,
|
|
24
48
|
handleTaskClaim,
|
|
25
49
|
handleTaskCreate,
|
|
@@ -125,6 +149,34 @@ export function startLocalHttpServer({ port, adapter, ctx }) {
|
|
|
125
149
|
const r = await handleTaskList(req, parseQuery(req.url || ''), cliCtx);
|
|
126
150
|
return writeJson(res, r.status, r.body);
|
|
127
151
|
}
|
|
152
|
+
if (urlNoQuery === '/agent/goal/report' && method === 'POST') {
|
|
153
|
+
const body = await readJsonBody(req);
|
|
154
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
155
|
+
const r = await handleGoalReport(req, body, cliCtx);
|
|
156
|
+
return writeJson(res, r.status, r.body);
|
|
157
|
+
}
|
|
158
|
+
if (urlNoQuery === '/agent/goal/changed' && method === 'POST') {
|
|
159
|
+
const body = await readJsonBody(req);
|
|
160
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
161
|
+
const r = await handleGoalChanged(req, body, cliCtx);
|
|
162
|
+
return writeJson(res, r.status, r.body);
|
|
163
|
+
}
|
|
164
|
+
if (urlNoQuery === '/agent/approval/request' && method === 'POST') {
|
|
165
|
+
const body = await readJsonBody(req);
|
|
166
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
167
|
+
const r = await handleApprovalRequest(req, body, cliCtx);
|
|
168
|
+
return writeJson(res, r.status, r.body);
|
|
169
|
+
}
|
|
170
|
+
if (urlNoQuery === '/agent/approval/resolve' && method === 'POST') {
|
|
171
|
+
const body = await readJsonBody(req);
|
|
172
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
173
|
+
const r = await handleApprovalResolve(req, body, cliCtx);
|
|
174
|
+
return writeJson(res, r.status, r.body);
|
|
175
|
+
}
|
|
176
|
+
if (urlNoQuery === '/agent/approval/list' && method === 'GET') {
|
|
177
|
+
const r = await handleApprovalList(req, parseQuery(req.url || ''), cliCtx);
|
|
178
|
+
return writeJson(res, r.status, r.body);
|
|
179
|
+
}
|
|
128
180
|
if (urlNoQuery === '/agent/message/react' && method === 'POST') {
|
|
129
181
|
const body = await readJsonBody(req);
|
|
130
182
|
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
@@ -223,6 +275,106 @@ export function startLocalHttpServer({ port, adapter, ctx }) {
|
|
|
223
275
|
const r = await handleServerInfo(req, parseQuery(req.url || ''), cliCtx);
|
|
224
276
|
return writeJson(res, r.status, r.body);
|
|
225
277
|
}
|
|
278
|
+
if (urlNoQuery === '/agent/workstream/charter/get' && method === 'GET') {
|
|
279
|
+
const r = await handleWorkstreamCharterGet(req, parseQuery(req.url || ''), cliCtx);
|
|
280
|
+
return writeJson(res, r.status, r.body);
|
|
281
|
+
}
|
|
282
|
+
if (urlNoQuery === '/agent/workstream/charter/set' && method === 'POST') {
|
|
283
|
+
const body = await readJsonBody(req);
|
|
284
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
285
|
+
const r = await handleWorkstreamCharterSet(req, body, cliCtx);
|
|
286
|
+
return writeJson(res, r.status, r.body);
|
|
287
|
+
}
|
|
288
|
+
if (urlNoQuery === '/agent/workstream/create' && method === 'POST') {
|
|
289
|
+
const body = await readJsonBody(req);
|
|
290
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
291
|
+
const r = await handleWorkstreamCreate(req, body, cliCtx);
|
|
292
|
+
return writeJson(res, r.status, r.body);
|
|
293
|
+
}
|
|
294
|
+
if (urlNoQuery === '/agent/workstream/delete' && method === 'POST') {
|
|
295
|
+
const body = await readJsonBody(req);
|
|
296
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
297
|
+
const r = await handleWorkstreamDelete(req, body, cliCtx);
|
|
298
|
+
return writeJson(res, r.status, r.body);
|
|
299
|
+
}
|
|
300
|
+
if (urlNoQuery === '/agent/workstream/list' && method === 'GET') {
|
|
301
|
+
const r = await handleWorkstreamList(req, parseQuery(req.url || ''), cliCtx);
|
|
302
|
+
return writeJson(res, r.status, r.body);
|
|
303
|
+
}
|
|
304
|
+
if (urlNoQuery === '/agent/agent/list' && method === 'GET') {
|
|
305
|
+
const r = await handleAgentList(req, parseQuery(req.url || ''), cliCtx);
|
|
306
|
+
return writeJson(res, r.status, r.body);
|
|
307
|
+
}
|
|
308
|
+
if (urlNoQuery === '/agent/agent/create' && method === 'POST') {
|
|
309
|
+
const body = await readJsonBody(req);
|
|
310
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
311
|
+
const r = await handleAgentCreate(req, body, cliCtx);
|
|
312
|
+
return writeJson(res, r.status, r.body);
|
|
313
|
+
}
|
|
314
|
+
if (urlNoQuery === '/agent/agent/delete' && method === 'POST') {
|
|
315
|
+
const body = await readJsonBody(req);
|
|
316
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
317
|
+
const r = await handleAgentDelete(req, body, cliCtx);
|
|
318
|
+
return writeJson(res, r.status, r.body);
|
|
319
|
+
}
|
|
320
|
+
if (urlNoQuery === '/agent/dashboard/set' && method === 'POST') {
|
|
321
|
+
const body = await readJsonBody(req);
|
|
322
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
323
|
+
const r = await handleWorkstreamDashboardSet(req, body, cliCtx);
|
|
324
|
+
return writeJson(res, r.status, r.body);
|
|
325
|
+
}
|
|
326
|
+
if (urlNoQuery === '/agent/dashboard/get' && method === 'GET') {
|
|
327
|
+
const r = await handleWorkstreamDashboardGet(req, parseQuery(req.url || ''), cliCtx);
|
|
328
|
+
return writeJson(res, r.status, r.body);
|
|
329
|
+
}
|
|
330
|
+
if (urlNoQuery === '/agent/briefing/publish' && method === 'POST') {
|
|
331
|
+
const body = await readJsonBody(req);
|
|
332
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
333
|
+
const r = await handleBriefingPublish(req, body, cliCtx);
|
|
334
|
+
return writeJson(res, r.status, r.body);
|
|
335
|
+
}
|
|
336
|
+
if (urlNoQuery === '/agent/briefing/get' && method === 'GET') {
|
|
337
|
+
const r = await handleBriefingGet(req, parseQuery(req.url || ''), cliCtx);
|
|
338
|
+
return writeJson(res, r.status, r.body);
|
|
339
|
+
}
|
|
340
|
+
if (urlNoQuery === '/agent/credential/request' && method === 'POST') {
|
|
341
|
+
const body = await readJsonBody(req);
|
|
342
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
343
|
+
const r = await handleCredentialRequest(req, body, cliCtx);
|
|
344
|
+
return writeJson(res, r.status, r.body);
|
|
345
|
+
}
|
|
346
|
+
if (urlNoQuery === '/agent/service/create' && method === 'POST') {
|
|
347
|
+
const body = await readJsonBody(req);
|
|
348
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
349
|
+
const r = await handleServiceCreate(req, body, cliCtx);
|
|
350
|
+
return writeJson(res, r.status, r.body);
|
|
351
|
+
}
|
|
352
|
+
if (urlNoQuery === '/agent/service/update' && method === 'POST') {
|
|
353
|
+
const body = await readJsonBody(req);
|
|
354
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
355
|
+
const r = await handleServiceUpdate(req, body, cliCtx);
|
|
356
|
+
return writeJson(res, r.status, r.body);
|
|
357
|
+
}
|
|
358
|
+
if (urlNoQuery === '/agent/service/delete' && method === 'POST') {
|
|
359
|
+
const body = await readJsonBody(req);
|
|
360
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
361
|
+
const r = await handleServiceDelete(req, body, cliCtx);
|
|
362
|
+
return writeJson(res, r.status, r.body);
|
|
363
|
+
}
|
|
364
|
+
if (urlNoQuery === '/agent/service/list' && method === 'GET') {
|
|
365
|
+
const r = await handleServiceList(req, parseQuery(req.url || ''), cliCtx);
|
|
366
|
+
return writeJson(res, r.status, r.body);
|
|
367
|
+
}
|
|
368
|
+
if (urlNoQuery === '/agent/service/info' && method === 'GET') {
|
|
369
|
+
const r = await handleServiceInfo(req, parseQuery(req.url || ''), cliCtx);
|
|
370
|
+
return writeJson(res, r.status, r.body);
|
|
371
|
+
}
|
|
372
|
+
if (urlNoQuery === '/agent/service/call' && method === 'POST') {
|
|
373
|
+
const body = await readJsonBody(req);
|
|
374
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
375
|
+
const r = await handleServiceCall(req, body, cliCtx);
|
|
376
|
+
return writeJson(res, r.status, r.body);
|
|
377
|
+
}
|
|
226
378
|
|
|
227
379
|
writeJson(res, 404, { error: 'not found' });
|
|
228
380
|
} 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,123 @@ export async function updateBindingRuntimeMeta(ctx, binding, runtimeMetaPatch, e
|
|
|
105
114
|
});
|
|
106
115
|
}
|
|
107
116
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
let pendingMeta = null;
|
|
115
|
-
let timer = null;
|
|
116
|
-
let flushChain = Promise.resolve();
|
|
117
|
-
|
|
118
|
-
const clearTimer = () => {
|
|
119
|
-
if (!timer) return;
|
|
120
|
-
clearTimeout(timer);
|
|
121
|
-
timer = null;
|
|
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
123
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
.
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
}
|
|
136
140
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
}
|
|
145
152
|
|
|
153
|
+
// The chat lane and the goal-FSM lane keep separate scoped session maps so a
|
|
154
|
+
// transition turn never resumes a user-chat runtime session (their per-step
|
|
155
|
+
// prompts and context differ). Lane is carried on the inbound; default chat.
|
|
156
|
+
function laneSessionsField(lane) {
|
|
157
|
+
return lane === 'goal' ? 'goalSessions' : 'chatSessions';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function resolveRuntimeSessionScope(meta = {}, inbound = {}) {
|
|
161
|
+
const lane = inbound?.lane === 'goal' ? 'goal' : 'chat';
|
|
162
|
+
const field = laneSessionsField(lane);
|
|
163
|
+
const key = readScopedSessionKey(inbound);
|
|
164
|
+
if (!key) {
|
|
165
|
+
return {
|
|
166
|
+
key: '',
|
|
167
|
+
lane,
|
|
168
|
+
field,
|
|
169
|
+
sessions: {},
|
|
170
|
+
sessionId: meta.sessionId || null,
|
|
171
|
+
path: meta.path || null,
|
|
172
|
+
lastRotatedAt: meta.lastRotatedAt || null,
|
|
173
|
+
shouldRotate: !meta.sessionId || Boolean(meta.rotatePending),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const sessions = meta.rotatePending ? {} : normalizeScopedRuntimeSessions(meta[field]);
|
|
178
|
+
const scoped = sessions[key] || {};
|
|
146
179
|
return {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
},
|
|
180
|
+
key,
|
|
181
|
+
lane,
|
|
182
|
+
field,
|
|
183
|
+
sessions,
|
|
184
|
+
sessionId: scoped.sessionId || null,
|
|
185
|
+
path: scoped.path || null,
|
|
186
|
+
lastRotatedAt: scoped.lastRotatedAt || null,
|
|
187
|
+
shouldRotate: !scoped.sessionId || Boolean(meta.rotatePending),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
178
190
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
191
|
+
export function buildRuntimeSessionMetaPatch(meta = {}, scope = {}, result = {}) {
|
|
192
|
+
const now = new Date().toISOString();
|
|
193
|
+
const scoped = Boolean(scope.key);
|
|
194
|
+
const lane = scope.lane === 'goal' ? 'goal' : 'chat';
|
|
195
|
+
const field = scope.field || laneSessionsField(lane);
|
|
196
|
+
const sessionId = result?.sessionId || scope.sessionId || (scoped ? null : meta.sessionId || null);
|
|
197
|
+
const path = result?.path || scope.path || (scoped ? null : meta.path || null);
|
|
198
|
+
const lastRotatedAt = scope.shouldRotate
|
|
199
|
+
? now
|
|
200
|
+
: (scope.lastRotatedAt || meta.lastRotatedAt || now);
|
|
201
|
+
|
|
202
|
+
if (!scoped) {
|
|
203
|
+
return {
|
|
204
|
+
sessionId,
|
|
205
|
+
...(path !== undefined ? { path } : {}),
|
|
206
|
+
rotatePending: false,
|
|
207
|
+
lastRotatedAt,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const sessions = {
|
|
212
|
+
...(scope.sessions || {}),
|
|
183
213
|
};
|
|
214
|
+
if (sessionId) {
|
|
215
|
+
sessions[scope.key] = {
|
|
216
|
+
sessionId,
|
|
217
|
+
path,
|
|
218
|
+
lastRotatedAt,
|
|
219
|
+
updatedAt: now,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const patch = {
|
|
224
|
+
[field]: pruneScopedRuntimeSessions(sessions),
|
|
225
|
+
rotatePending: false,
|
|
226
|
+
lastRotatedAt,
|
|
227
|
+
};
|
|
228
|
+
// The flat sessionId/path is the chat lane's "last used" mirror, read only
|
|
229
|
+
// by non-scoped resolution and display. The goal lane owns its own map and
|
|
230
|
+
// must not clobber that mirror.
|
|
231
|
+
if (lane === 'chat') {
|
|
232
|
+
patch.sessionId = sessionId;
|
|
233
|
+
patch.path = path;
|
|
234
|
+
}
|
|
235
|
+
return patch;
|
|
184
236
|
}
|