ticlawk 0.1.17-dev.2 → 0.1.17-dev.20
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 +26 -59
- package/bin/ticlawk.mjs +31 -301
- package/package.json +4 -2
- package/scripts/publish-dev.sh +77 -0
- package/src/adapters/ticlawk/api.mjs +50 -378
- package/src/adapters/ticlawk/credentials.mjs +1 -43
- package/src/adapters/ticlawk/index.mjs +61 -565
- package/src/adapters/ticlawk/wake-client.mjs +1 -1
- package/src/cli/agent-commands.mjs +18 -715
- package/src/core/adapter-registry.mjs +1 -19
- package/src/core/agent-cli-handlers.mjs +18 -556
- package/src/core/agent-home.mjs +1 -81
- package/src/core/events/worker-events.mjs +36 -32
- package/src/core/http.mjs +0 -152
- package/src/core/profiles.mjs +0 -1
- package/src/core/runtime-contract.mjs +1 -0
- package/src/core/runtime-env.mjs +0 -8
- package/src/core/runtime-support.mjs +78 -130
- package/src/runtimes/_shared/incoming-message-prompt.mjs +232 -0
- package/src/runtimes/_shared/runtime-base-instructions.mjs +34 -0
- package/src/runtimes/claude-code/index.mjs +48 -21
- package/src/runtimes/claude-code/session.mjs +7 -2
- package/src/runtimes/codex/index.mjs +64 -116
- package/src/runtimes/codex/session.mjs +12 -2
- package/src/runtimes/openclaw/index.mjs +30 -17
- package/src/runtimes/opencode/index.mjs +64 -42
- package/src/runtimes/opencode/session.mjs +14 -14
- package/src/runtimes/pi/index.mjs +64 -42
- package/src/runtimes/pi/session.mjs +8 -11
- package/ticlawk.mjs +32 -5
- package/src/runtimes/_shared/agent-handbook.mjs +0 -45
- package/src/runtimes/_shared/brand.mjs +0 -2
- package/src/runtimes/_shared/goal-step-prompt.mjs +0 -98
- package/src/runtimes/_shared/goal-task-protocol.mjs +0 -50
- package/src/runtimes/_shared/handbook/BASICS.md +0 -27
- package/src/runtimes/_shared/handbook/COLLABORATION.md +0 -37
- package/src/runtimes/_shared/handbook/COMMUNICATION.md +0 -55
- package/src/runtimes/_shared/handbook/DM_SCOPE.md +0 -13
- package/src/runtimes/_shared/handbook/GOAL_AUTHORITY.md +0 -47
- package/src/runtimes/_shared/handbook/GOAL_TASK_CORE.md +0 -43
- package/src/runtimes/_shared/handbook/GROUP_ADMIN_SCOPE.md +0 -21
- package/src/runtimes/_shared/handbook/GROUP_MEMBER_SCOPE.md +0 -15
- package/src/runtimes/_shared/handbook/SURFACES.md +0 -41
- package/src/runtimes/_shared/handbook/TASK_WORKER.md +0 -14
- package/src/runtimes/_shared/standing-prompt.mjs +0 -171
- package/src/runtimes/_shared/wake-prompt.mjs +0 -268
package/src/core/agent-home.mjs
CHANGED
|
@@ -7,10 +7,9 @@
|
|
|
7
7
|
* lives in cwd, agent reads it via `cat MEMORY.md`.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { existsSync,
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, 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';
|
|
14
13
|
|
|
15
14
|
export const AF_AGENTS_DIR = join(AF_HOME, 'agents');
|
|
16
15
|
|
|
@@ -38,88 +37,9 @@ export function ensureAgentHome(agentId, { displayName } = {}) {
|
|
|
38
37
|
if (!existsSync(memoryPath)) {
|
|
39
38
|
writeFileSync(memoryPath, buildInitialMemoryMd({ displayName, home }), 'utf8');
|
|
40
39
|
}
|
|
41
|
-
writeManagedHandbookFiles(home);
|
|
42
|
-
ensureSkillSymlinks(home);
|
|
43
40
|
return home;
|
|
44
41
|
}
|
|
45
42
|
|
|
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
|
-
|
|
123
43
|
function buildInitialMemoryMd({ displayName, home }) {
|
|
124
44
|
const lines = [
|
|
125
45
|
`# ${displayName || 'Agent'}`,
|
|
@@ -1,9 +1,22 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
1
3
|
const workerEventSeq = new Map();
|
|
4
|
+
const DELTA_LOG_PREVIEW_CHARS = 48;
|
|
2
5
|
|
|
3
6
|
function shortId(value) {
|
|
4
7
|
return value ? String(value).slice(0, 8) : null;
|
|
5
8
|
}
|
|
6
9
|
|
|
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
|
+
|
|
7
20
|
function nextWorkerEventMeta({ agent, sessionId, turnId }) {
|
|
8
21
|
const key = `${agent}:${sessionId || ''}:${turnId || 'session'}`;
|
|
9
22
|
const seq = (workerEventSeq.get(key) || 0) + 1;
|
|
@@ -24,16 +37,6 @@ export async function emitWorkerEvent({ adapter, binding, agent, sessionId, cwd
|
|
|
24
37
|
if (!sessionId || typeof adapter.emitEvent !== 'function') return;
|
|
25
38
|
const eventName = event?.worker_event_name || event?.hook_event_name || 'unknown';
|
|
26
39
|
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
|
-
}
|
|
37
40
|
const { key, seq, originTs } = nextWorkerEventMeta({ agent, sessionId, turnId: logicalTurnId });
|
|
38
41
|
const enrichedEvent = {
|
|
39
42
|
...event,
|
|
@@ -42,14 +45,29 @@ export async function emitWorkerEvent({ adapter, binding, agent, sessionId, cwd
|
|
|
42
45
|
event_seq: seq,
|
|
43
46
|
origin_ts: originTs,
|
|
44
47
|
};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
if (typeof event?.delta === 'string' && event.delta) {
|
|
49
|
+
enrichedEvent.delta_chars = event.delta.length;
|
|
50
|
+
enrichedEvent.delta_hash = hashText(event.delta);
|
|
51
|
+
logger?.debugLog?.('events', 'delta.recv', {
|
|
52
|
+
bindingId: binding.id,
|
|
53
|
+
agent,
|
|
54
|
+
sessionId: shortId(sessionId),
|
|
55
|
+
turnId: shortId(logicalTurnId),
|
|
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
|
+
}
|
|
53
71
|
await adapter.emitEvent(binding, {
|
|
54
72
|
agent,
|
|
55
73
|
sessionId,
|
|
@@ -60,17 +78,3 @@ export async function emitWorkerEvent({ adapter, binding, agent, sessionId, cwd
|
|
|
60
78
|
clearWorkerEventMeta(key);
|
|
61
79
|
}
|
|
62
80
|
}
|
|
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,25 +3,6 @@ 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,
|
|
25
6
|
handleGroupMembers,
|
|
26
7
|
handleGroupMembersAdd,
|
|
27
8
|
handleGroupMembersRemove,
|
|
@@ -39,11 +20,6 @@ import {
|
|
|
39
20
|
handleReminderSchedule,
|
|
40
21
|
handleReminderSnooze,
|
|
41
22
|
handleReminderUpdate,
|
|
42
|
-
handleGoalChanged,
|
|
43
|
-
handleGoalReport,
|
|
44
|
-
handleApprovalRequest,
|
|
45
|
-
handleApprovalResolve,
|
|
46
|
-
handleApprovalList,
|
|
47
23
|
handleServerInfo,
|
|
48
24
|
handleTaskClaim,
|
|
49
25
|
handleTaskCreate,
|
|
@@ -149,34 +125,6 @@ export function startLocalHttpServer({ port, adapter, ctx }) {
|
|
|
149
125
|
const r = await handleTaskList(req, parseQuery(req.url || ''), cliCtx);
|
|
150
126
|
return writeJson(res, r.status, r.body);
|
|
151
127
|
}
|
|
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
|
-
}
|
|
180
128
|
if (urlNoQuery === '/agent/message/react' && method === 'POST') {
|
|
181
129
|
const body = await readJsonBody(req);
|
|
182
130
|
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
@@ -275,106 +223,6 @@ export function startLocalHttpServer({ port, adapter, ctx }) {
|
|
|
275
223
|
const r = await handleServerInfo(req, parseQuery(req.url || ''), cliCtx);
|
|
276
224
|
return writeJson(res, r.status, r.body);
|
|
277
225
|
}
|
|
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
|
-
}
|
|
378
226
|
|
|
379
227
|
writeJson(res, 404, { error: 'not found' });
|
|
380
228
|
} catch (err) {
|
package/src/core/profiles.mjs
CHANGED
|
@@ -92,7 +92,6 @@ export function saveProfile({ adapter, userId, config = {}, meta = {} }) {
|
|
|
92
92
|
mkdirSync(dir, { recursive: true });
|
|
93
93
|
const currentConfig = readProfileConfig(adapter, userId);
|
|
94
94
|
const nextConfig = { ...currentConfig, ...config };
|
|
95
|
-
delete nextConfig.TICLAWK_SETUP_CODE;
|
|
96
95
|
writeDotenvAtomic(getProfileConfigPath(adapter, userId), nextConfig);
|
|
97
96
|
|
|
98
97
|
const currentMeta = readProfileMeta(adapter, userId) || {};
|
|
@@ -55,6 +55,7 @@ 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]
|
|
58
59
|
*/
|
|
59
60
|
|
|
60
61
|
/**
|
package/src/core/runtime-env.mjs
CHANGED
|
@@ -15,8 +15,6 @@
|
|
|
15
15
|
const STRIPPED_KEYS = new Set([
|
|
16
16
|
'TICLAWK_CONNECTOR_API_KEY',
|
|
17
17
|
'TICLAWK_CONNECTOR_WS_URL',
|
|
18
|
-
'TICLAWK_CREDENTIAL_NAMES',
|
|
19
|
-
'TICLAWK_SETUP_CODE',
|
|
20
18
|
]);
|
|
21
19
|
|
|
22
20
|
export function buildRuntimeEnv(extra = {}) {
|
|
@@ -36,17 +34,11 @@ export function buildAgentRuntimeEnv({
|
|
|
36
34
|
sessionId,
|
|
37
35
|
hostId,
|
|
38
36
|
daemonUrl,
|
|
39
|
-
conversationId,
|
|
40
|
-
messageId,
|
|
41
|
-
target,
|
|
42
37
|
} = {}) {
|
|
43
38
|
const out = {};
|
|
44
39
|
if (agentId) out.TICLAWK_RUNTIME_AGENT_ID = String(agentId);
|
|
45
40
|
if (sessionId) out.TICLAWK_RUNTIME_SESSION_ID = String(sessionId);
|
|
46
41
|
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);
|
|
50
42
|
out.TICLAWK_RUNTIME_DAEMON_URL = daemonUrl || 'http://127.0.0.1:8741';
|
|
51
43
|
return out;
|
|
52
44
|
}
|
|
@@ -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';
|
|
4
3
|
const ERROR_MAX_CHARS = 500;
|
|
5
|
-
const
|
|
4
|
+
const DEFAULT_DELTA_FLUSH_MS = 250;
|
|
5
|
+
const DEFAULT_DELTA_FLUSH_CHARS = 64;
|
|
6
6
|
|
|
7
7
|
function truncateError(text) {
|
|
8
8
|
if (!text) return null;
|
|
@@ -74,21 +74,12 @@ 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
|
-
|
|
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
|
-
}
|
|
77
|
+
await adapter.postAgentReply(binding, {
|
|
78
|
+
conversationId: inbound?.conversationId || null,
|
|
79
|
+
text,
|
|
80
|
+
replyToMessageId: inbound?.messageId || null,
|
|
81
|
+
visibility: 'admin',
|
|
82
|
+
});
|
|
92
83
|
}
|
|
93
84
|
|
|
94
85
|
export function terminalRuntimeFailure(reason = 'runtime failure') {
|
|
@@ -114,123 +105,80 @@ export async function updateBindingRuntimeMeta(ctx, binding, runtimeMetaPatch, e
|
|
|
114
105
|
});
|
|
115
106
|
}
|
|
116
107
|
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (!
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
}
|
|
140
|
-
|
|
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
|
-
// 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] || {};
|
|
179
|
-
return {
|
|
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),
|
|
108
|
+
export function createDeltaAggregator({
|
|
109
|
+
flushDelta,
|
|
110
|
+
flushMs = DEFAULT_DELTA_FLUSH_MS,
|
|
111
|
+
flushChars = DEFAULT_DELTA_FLUSH_CHARS,
|
|
112
|
+
}) {
|
|
113
|
+
let buffer = '';
|
|
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;
|
|
188
122
|
};
|
|
189
|
-
}
|
|
190
123
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
sessionId,
|
|
205
|
-
...(path !== undefined ? { path } : {}),
|
|
206
|
-
rotatePending: false,
|
|
207
|
-
lastRotatedAt,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
124
|
+
const startFlush = () => {
|
|
125
|
+
if (!buffer || typeof flushDelta !== 'function') return flushChain;
|
|
126
|
+
const text = buffer;
|
|
127
|
+
const meta = pendingMeta || {};
|
|
128
|
+
buffer = '';
|
|
129
|
+
pendingMeta = null;
|
|
130
|
+
clearTimer();
|
|
131
|
+
flushChain = flushChain
|
|
132
|
+
.catch(() => {})
|
|
133
|
+
.then(() => flushDelta({ text, ...meta }));
|
|
134
|
+
return flushChain;
|
|
135
|
+
};
|
|
210
136
|
|
|
211
|
-
const
|
|
212
|
-
|
|
137
|
+
const scheduleFlush = () => {
|
|
138
|
+
if (!buffer || timer) return;
|
|
139
|
+
timer = setTimeout(() => {
|
|
140
|
+
timer = null;
|
|
141
|
+
void startFlush();
|
|
142
|
+
}, flushMs);
|
|
143
|
+
timer.unref?.();
|
|
213
144
|
};
|
|
214
|
-
if (sessionId) {
|
|
215
|
-
sessions[scope.key] = {
|
|
216
|
-
sessionId,
|
|
217
|
-
path,
|
|
218
|
-
lastRotatedAt,
|
|
219
|
-
updatedAt: now,
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
145
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
146
|
+
return {
|
|
147
|
+
push(text, meta = {}) {
|
|
148
|
+
const normalized = typeof text === 'string' ? text : '';
|
|
149
|
+
if (!normalized) return;
|
|
150
|
+
|
|
151
|
+
const nextMeta = {
|
|
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
|
+
},
|
|
227
183
|
};
|
|
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;
|
|
236
184
|
}
|