@metagptx/deepflow-suite-cli 0.2.10 → 0.3.0
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 +44 -50
- package/bin/deepflow-suite.js +4 -4
- package/bundle/deepflow-release.json +21 -91
- package/bundle/skills/deepflow-suite/SKILL.md +7 -1
- package/bundle/skills/df-agentctrl/SKILL.md +92 -10
- package/bundle/skills/df-agentrt/SKILL.md +65 -14
- package/bundle/skills/df-agentrt-repo/SKILL.md +4 -0
- package/bundle/skills/df-task/SKILL.md +98 -26
- package/bundle/skills/df-taskdriver/SKILL.md +8 -4
- package/bundle/skills/df-workbench/SKILL.md +5 -9
- package/manifest.json +12 -30
- package/package.json +8 -17
- package/runtime/df-agentctrl/commands.mjs +93 -0
- package/runtime/df-agentctrl/df-agentctrl.mjs +119 -0
- package/runtime/df-agentctrl/release.json +1 -0
- package/runtime/df-agentrt/agentrt.mjs +3 -0
- package/runtime/df-agentrt/df-agentrt.mjs +3 -0
- package/runtime/df-agentrt/release.json +1 -0
- package/runtime/df-agentrt/runtime.mjs +326 -0
- package/runtime/df-agentrt/usage.mjs +16 -0
- package/runtime/df-task/commands.mjs +181 -0
- package/runtime/df-task/df-task.mjs +155 -0
- package/runtime/df-task/protocol.mjs +3 -0
- package/runtime/df-task/release.json +1 -0
- package/runtime/df-taskdriver/df-taskdriver.mjs +131 -0
- package/runtime/df-taskdriver/release.json +1 -0
- package/runtime/{df-workbench.mjs → df-workbench/df-workbench.mjs} +17 -175
- package/runtime/df-workbench/release.json +1 -0
- package/scripts/build-package.mjs +29 -25
- package/src/bundle.mjs +4 -1
- package/src/cli-install.mjs +35 -125
- package/src/commands/doctor.mjs +2 -0
- package/src/linker.mjs +2 -2
- package/src/manifest.mjs +7 -13
- package/src/package-cli.mjs +44 -60
- package/bin/df-agentctrl.js +0 -10
- package/bin/df-agentrt.js +0 -10
- package/bin/df-task.js +0 -10
- package/bin/df-taskdriver.js +0 -10
- package/bin/df-workbench.js +0 -10
- package/bundle/skills/deepflow-suite/manifest.yaml +0 -20
- package/bundle/skills/df-agentctrl/references/environment.md +0 -40
- package/bundle/skills/df-agentctrl/references/query-scope.md +0 -50
- package/bundle/skills/df-agentrt/references/environment.md +0 -29
- package/bundle/skills/df-agentrt/references/invocation-result.md +0 -11
- package/bundle/skills/df-agentrt/references/query-scope.md +0 -30
- package/bundle/skills/df-agentrt/references/session-create.md +0 -14
- package/bundle/skills/df-task/references/environment.md +0 -50
- package/bundle/skills/df-task/references/query-scope.md +0 -49
- package/bundle/skills/df-task/references/reading.md +0 -9
- package/bundle/skills/df-task/skill_test.go +0 -201
- package/bundle/skills/df-workbench/manifest.yaml +0 -5
- package/scripts/build-runtime-packages.mjs +0 -38
- package/skills/deepflow-suite/SKILL.md +0 -81
- package/skills/deepflow-suite/manifest.yaml +0 -20
- package/src/cli-launcher.mjs +0 -71
- package/src/runtime-build.mjs +0 -193
- package/src/runtime-package.mjs +0 -31
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
export const help = `df-task — DeepFlow Task fact log CLI
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
df-task ls [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]
|
|
5
|
+
df-task get <task-id> [--metadata-source <source>] [--metadata-query-key <key>]
|
|
6
|
+
df-task create --title <text> [--description <text>] [--message <text>] [--metadata <json>]
|
|
7
|
+
df-task facts <task-id> [--after <seq>] [--types <a,b>] [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]
|
|
8
|
+
df-task append <task-id> --type <type> --file payload.json [--idempotency-key <k>]
|
|
9
|
+
df-task stream [--after <seq>] [--types <a,b>] [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]
|
|
10
|
+
df-task title <task-id> --title <text>
|
|
11
|
+
df-task metadata <task-id> --metadata <json> [--metadata-source <source>] [--metadata-query-key <key>]
|
|
12
|
+
|
|
13
|
+
Scheduling shorthands (each one is a wrapper around append):
|
|
14
|
+
df-task dispatch <task-id> --agent <name> --instruction <text> [--run <run-id>]
|
|
15
|
+
df-task wait <task-id> --on <what> --reason <text> [--waiting-run <run-id>]
|
|
16
|
+
df-task handoff <task-id> --ask <text> [--assignee <who>]
|
|
17
|
+
|
|
18
|
+
Global options (before or after the command):
|
|
19
|
+
--base-url <url> Service URL (or DF_TASK_BASE_URL)
|
|
20
|
+
--json Machine-readable JSON output
|
|
21
|
+
help [command] Show general or command help (-h / --help also supported)
|
|
22
|
+
|
|
23
|
+
Environment:
|
|
24
|
+
DF_TASK_BASE_URL Service URL, default http://127.0.0.1:8090
|
|
25
|
+
DF_TASK_ACTOR_KIND Write identity: human | external | task_run | scheduler_run | driver
|
|
26
|
+
DF_TASK_ACTOR_ID Write identity id, e.g. the current run id
|
|
27
|
+
|
|
28
|
+
Facts are append-only. The API has no way to change or remove a fact, so neither does this CLI.
|
|
29
|
+
`;
|
|
30
|
+
export const commands = {
|
|
31
|
+
"ls": {
|
|
32
|
+
"usage": "ls [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]",
|
|
33
|
+
"description": "List tasks, newest first.",
|
|
34
|
+
"flags": [
|
|
35
|
+
"--limit",
|
|
36
|
+
"--metadata-source",
|
|
37
|
+
"--metadata-query-key"
|
|
38
|
+
],
|
|
39
|
+
"required": [],
|
|
40
|
+
"needsID": false,
|
|
41
|
+
"writes": false
|
|
42
|
+
},
|
|
43
|
+
"get": {
|
|
44
|
+
"usage": "get <task-id> [--metadata-source <source>] [--metadata-query-key <key>]",
|
|
45
|
+
"description": "Read one task's identity.",
|
|
46
|
+
"flags": [
|
|
47
|
+
"--metadata-source",
|
|
48
|
+
"--metadata-query-key"
|
|
49
|
+
],
|
|
50
|
+
"required": [],
|
|
51
|
+
"needsID": true,
|
|
52
|
+
"writes": false
|
|
53
|
+
},
|
|
54
|
+
"create": {
|
|
55
|
+
"usage": "create --title <text> [--description <text>] [--message <text>] [--metadata <json>]",
|
|
56
|
+
"description": "Create a task, optionally with its first user message in the same transaction.",
|
|
57
|
+
"flags": [
|
|
58
|
+
"--title",
|
|
59
|
+
"--description",
|
|
60
|
+
"--message",
|
|
61
|
+
"--metadata"
|
|
62
|
+
],
|
|
63
|
+
"required": [
|
|
64
|
+
"--title"
|
|
65
|
+
],
|
|
66
|
+
"needsID": false,
|
|
67
|
+
"writes": true
|
|
68
|
+
},
|
|
69
|
+
"facts": {
|
|
70
|
+
"usage": "facts <task-id> [--after <seq>] [--types <a,b>] [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]",
|
|
71
|
+
"description": "Read one task's facts in seq order.",
|
|
72
|
+
"flags": [
|
|
73
|
+
"--after",
|
|
74
|
+
"--types",
|
|
75
|
+
"--limit",
|
|
76
|
+
"--metadata-source",
|
|
77
|
+
"--metadata-query-key"
|
|
78
|
+
],
|
|
79
|
+
"required": [],
|
|
80
|
+
"needsID": true,
|
|
81
|
+
"writes": false
|
|
82
|
+
},
|
|
83
|
+
"append": {
|
|
84
|
+
"usage": "append <task-id> --type <type> --file payload.json [--idempotency-key <k>]",
|
|
85
|
+
"description": "Append one fact from a JSON payload file.",
|
|
86
|
+
"flags": [
|
|
87
|
+
"--type",
|
|
88
|
+
"--file",
|
|
89
|
+
"--idempotency-key"
|
|
90
|
+
],
|
|
91
|
+
"required": [
|
|
92
|
+
"--type",
|
|
93
|
+
"--file"
|
|
94
|
+
],
|
|
95
|
+
"needsID": true,
|
|
96
|
+
"writes": true
|
|
97
|
+
},
|
|
98
|
+
"stream": {
|
|
99
|
+
"usage": "stream [--after <seq>] [--types <a,b>] [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]",
|
|
100
|
+
"description": "Read facts across all tasks from a cursor. One-shot pull, not a long connection.",
|
|
101
|
+
"flags": [
|
|
102
|
+
"--after",
|
|
103
|
+
"--types",
|
|
104
|
+
"--limit",
|
|
105
|
+
"--metadata-source",
|
|
106
|
+
"--metadata-query-key"
|
|
107
|
+
],
|
|
108
|
+
"required": [],
|
|
109
|
+
"needsID": false,
|
|
110
|
+
"writes": false
|
|
111
|
+
},
|
|
112
|
+
"title": {
|
|
113
|
+
"usage": "title <task-id> --title <text>",
|
|
114
|
+
"description": "Change a task's title. Writes no fact.",
|
|
115
|
+
"flags": [
|
|
116
|
+
"--title"
|
|
117
|
+
],
|
|
118
|
+
"required": [
|
|
119
|
+
"--title"
|
|
120
|
+
],
|
|
121
|
+
"needsID": true,
|
|
122
|
+
"writes": true
|
|
123
|
+
},
|
|
124
|
+
"metadata": {
|
|
125
|
+
"usage": "metadata <task-id> --metadata <json> [--metadata-source <source>] [--metadata-query-key <key>]",
|
|
126
|
+
"description": "Replace a task's metadata as human. Writes no fact.",
|
|
127
|
+
"flags": [
|
|
128
|
+
"--metadata",
|
|
129
|
+
"--metadata-source",
|
|
130
|
+
"--metadata-query-key"
|
|
131
|
+
],
|
|
132
|
+
"required": [
|
|
133
|
+
"--metadata"
|
|
134
|
+
],
|
|
135
|
+
"needsID": true,
|
|
136
|
+
"writes": true
|
|
137
|
+
},
|
|
138
|
+
"dispatch": {
|
|
139
|
+
"usage": "dispatch <task-id> --agent <name> --instruction <text> [--run <run-id>]",
|
|
140
|
+
"description": "Append a dispatch intent. Without --run it asks for a new run; with --run it posts a message to that run.",
|
|
141
|
+
"flags": [
|
|
142
|
+
"--agent",
|
|
143
|
+
"--instruction",
|
|
144
|
+
"--run"
|
|
145
|
+
],
|
|
146
|
+
"required": [
|
|
147
|
+
"--agent",
|
|
148
|
+
"--instruction"
|
|
149
|
+
],
|
|
150
|
+
"needsID": true,
|
|
151
|
+
"writes": true
|
|
152
|
+
},
|
|
153
|
+
"wait": {
|
|
154
|
+
"usage": "wait <task-id> --on <what> --reason <text> [--waiting-run <run-id>]",
|
|
155
|
+
"description": "Append a waiting reason. It is context for humans and the next scheduling run, not a gate for the driver.",
|
|
156
|
+
"flags": [
|
|
157
|
+
"--on",
|
|
158
|
+
"--reason",
|
|
159
|
+
"--waiting-run"
|
|
160
|
+
],
|
|
161
|
+
"required": [
|
|
162
|
+
"--on",
|
|
163
|
+
"--reason"
|
|
164
|
+
],
|
|
165
|
+
"needsID": true,
|
|
166
|
+
"writes": true
|
|
167
|
+
},
|
|
168
|
+
"handoff": {
|
|
169
|
+
"usage": "handoff <task-id> --ask <text> [--assignee <who>]",
|
|
170
|
+
"description": "Append a handoff to a human. Handing the ball back and asking a human to do a step are the same fact.",
|
|
171
|
+
"flags": [
|
|
172
|
+
"--ask",
|
|
173
|
+
"--assignee"
|
|
174
|
+
],
|
|
175
|
+
"required": [
|
|
176
|
+
"--ask"
|
|
177
|
+
],
|
|
178
|
+
"needsID": true,
|
|
179
|
+
"writes": true
|
|
180
|
+
}
|
|
181
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { realpathSync } from 'node:fs';
|
|
5
|
+
import { help, commands } from './commands.mjs';
|
|
6
|
+
import { factTypes, actorKinds } from './protocol.mjs';
|
|
7
|
+
|
|
8
|
+
const uuid = /^(?:urn:uuid:)?(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\})$/i;
|
|
9
|
+
const valueFlags = new Set(['--base-url', ...Object.values(commands).flatMap((spec) => spec.flags)]);
|
|
10
|
+
export function parse(args) {
|
|
11
|
+
const o = { command: '', args: [], values: {}, json: false, help: false };
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
const arg = args[i];
|
|
15
|
+
if (!arg.startsWith('-')) { if (!o.command) o.command = arg; else o.args.push(arg); continue; }
|
|
16
|
+
const equals = arg.indexOf('=');
|
|
17
|
+
const key = equals < 0 ? arg : arg.slice(0, equals);
|
|
18
|
+
if (seen.has(key)) throw Error(`duplicate option ${key}`);
|
|
19
|
+
seen.add(key);
|
|
20
|
+
if (['--help', '-h', '--json'].includes(key)) {
|
|
21
|
+
if (equals >= 0) throw Error(`${key} does not take a value`);
|
|
22
|
+
o[key === '--json' ? 'json' : 'help'] = true;
|
|
23
|
+
} else if (valueFlags.has(key)) {
|
|
24
|
+
if (equals < 0 && (i + 1 === args.length || args[i + 1].startsWith('--'))) throw Error(`${key} requires a value`);
|
|
25
|
+
o.values[key] = equals < 0 ? args[++i] : arg.slice(equals + 1);
|
|
26
|
+
} else throw Error(`unknown option ${key}`);
|
|
27
|
+
}
|
|
28
|
+
o.command ||= 'help';
|
|
29
|
+
if (o.command === 'help' && o.args.length === 1) { o.command = o.args.pop(); o.help = true; }
|
|
30
|
+
return o;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function validate(o, env) {
|
|
34
|
+
const spec = Object.hasOwn(commands, o.command) ? commands[o.command] : undefined;
|
|
35
|
+
if (!spec) throw Error(`unknown command ${o.command}`);
|
|
36
|
+
for (const key of Object.keys(o.values)) if (key !== '--base-url' && !spec.flags.includes(key)) throw Error(`${key} is not supported by ${o.command}`);
|
|
37
|
+
for (const key of spec.required) if (!o.values[key]?.trim()) throw Error(`${o.command} requires ${key}`);
|
|
38
|
+
if (spec.needsID) {
|
|
39
|
+
if (o.args.length !== 1) throw Error(`${o.command} requires exactly one task-id`);
|
|
40
|
+
if (!uuid.test(o.args[0])) throw Error('task-id must be a UUID');
|
|
41
|
+
} else if (o.args.length) throw Error(`${o.command} does not accept positional arguments`);
|
|
42
|
+
for (const key of ['--type', '--types']) if (key in o.values) {
|
|
43
|
+
for (const raw of o.values[key].split(',')) {
|
|
44
|
+
const name = raw.trim();
|
|
45
|
+
if (!name) throw Error('--types must not contain an empty entry');
|
|
46
|
+
if (!factTypes.includes(name)) throw Error(`unknown fact type ${name}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if ('--run' in o.values && !o.values['--run'].trim()) throw Error('--run must not be empty');
|
|
50
|
+
if ('--metadata' in o.values) object(o.values['--metadata'], '--metadata must be a JSON object');
|
|
51
|
+
for (const key of ['--limit', '--after']) if (key in o.values) {
|
|
52
|
+
const raw = o.values[key];
|
|
53
|
+
if (!/^[+]?\d+$/.test(raw) || BigInt(raw) > 9223372036854775807n || (key === '--limit' && BigInt(raw) === 0n)) throw Error(`${key} must be a non-negative integer`);
|
|
54
|
+
}
|
|
55
|
+
if (spec.writes) {
|
|
56
|
+
if (!env.DF_TASK_ACTOR_KIND?.trim() || !env.DF_TASK_ACTOR_ID?.trim()) throw Error('writing a fact requires DF_TASK_ACTOR_KIND and DF_TASK_ACTOR_ID');
|
|
57
|
+
if (!actorKinds.includes(env.DF_TASK_ACTOR_KIND)) throw Error(`unknown DF_TASK_ACTOR_KIND ${env.DF_TASK_ACTOR_KIND}`);
|
|
58
|
+
if (o.command === 'metadata' && env.DF_TASK_ACTOR_KIND !== 'human') throw Error('metadata requires DF_TASK_ACTOR_KIND=human');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function object(raw, message) {
|
|
62
|
+
let value;
|
|
63
|
+
try { value = JSON.parse(raw); } catch { throw Error(message); }
|
|
64
|
+
if (!value || Array.isArray(value) || typeof value !== 'object') throw Error(message);
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function query(values) {
|
|
68
|
+
const pairs = Object.entries(values).filter(([, value]) => value !== undefined && value !== '');
|
|
69
|
+
pairs.sort(([a], [b]) => a.localeCompare(b));
|
|
70
|
+
const q = new URLSearchParams(pairs).toString();
|
|
71
|
+
return q ? `?${q}` : '';
|
|
72
|
+
}
|
|
73
|
+
export async function requestFor(o) {
|
|
74
|
+
const v = o.values, c = o.command;
|
|
75
|
+
const path = o.args.length ? `/api/v1/tasks/${encodeURIComponent(o.args[0])}` : '/api/v1/tasks';
|
|
76
|
+
const metadata = { metadata_source: v['--metadata-source'], metadata_query_key: v['--metadata-query-key'] };
|
|
77
|
+
const post = (p, body) => ({ method: 'POST', path: p, body: JSON.stringify(body) });
|
|
78
|
+
if (c === 'ls') return { method: 'GET', path: path + query({ ...metadata, limit: v['--limit'] || '50' }) };
|
|
79
|
+
if (c === 'get') return { method: 'GET', path: path + query(metadata) };
|
|
80
|
+
if (c === 'facts' || c === 'stream') return { method: 'GET', path: (c === 'facts' ? `${path}/facts` : '/api/v1/facts') + query({ ...metadata, after_seq: v['--after'], types: v['--types'], limit: v['--limit'] }) };
|
|
81
|
+
// Preserve user JSON bytes, including integer precision and unknown fields.
|
|
82
|
+
if (c === 'create') return { method: 'POST', path, body: `{"title":${JSON.stringify(v['--title'])},"description":${JSON.stringify(v['--description'] || '')}${v['--message'] ? `,"first_message":${JSON.stringify(v['--message'])}` : ''}${v['--metadata'] ? `,"metadata":${v['--metadata']}` : ''}}` };
|
|
83
|
+
if (c === 'title') return post(`${path}/title`, { title: v['--title'] });
|
|
84
|
+
if (c === 'metadata') return { method: 'POST', path: `${path}/metadata${query(metadata)}`, body: `{"metadata":${v['--metadata']}}` };
|
|
85
|
+
if (c === 'append') {
|
|
86
|
+
const raw = await readFile(v['--file'], 'utf8');
|
|
87
|
+
object(raw, `${v['--file']} must contain a JSON object`);
|
|
88
|
+
return { method: 'POST', path: `${path}/facts`, body: `{"type":${JSON.stringify(v['--type'])},"payload":${raw}${v['--idempotency-key'] ? `,"idempotency_key":${JSON.stringify(v['--idempotency-key'])}` : ''}}` };
|
|
89
|
+
}
|
|
90
|
+
let type, payload;
|
|
91
|
+
if (c === 'dispatch') { type = 'dispatch_intent'; payload = { agent: v['--agent'], instruction: v['--instruction'], mode: v['--run'] ? 'message' : 'new_run', ...(v['--run'] ? { target_run_id: v['--run'] } : {}) }; }
|
|
92
|
+
if (c === 'wait') { type = 'waiting_reason'; payload = { waiting_on: v['--on'], reason: v['--reason'], ...(v['--waiting-run'] ? { waiting_run_id: v['--waiting-run'] } : {}) }; }
|
|
93
|
+
if (c === 'handoff') { type = 'handoff_to_human'; payload = { ask: v['--ask'], ...(v['--assignee'] ? { assignee: v['--assignee'] } : {}) }; }
|
|
94
|
+
return post(`${path}/facts`, { type, payload });
|
|
95
|
+
}
|
|
96
|
+
function human(command, data, stdout) {
|
|
97
|
+
const page = JSON.parse(data, (_key, value, context) => typeof value === 'number' && !Number.isSafeInteger(value) && /^-?\d+$/.test(context.source) ? BigInt(context.source) : value) || {};
|
|
98
|
+
if (typeof page !== 'object' || Array.isArray(page) || (page.tasks != null && !Array.isArray(page.tasks)) || (page.facts != null && !Array.isArray(page.facts))) { stdout.write(pretty(data) + '\n'); return; }
|
|
99
|
+
if (command === 'ls') {
|
|
100
|
+
for (const task of page.tasks || []) stdout.write(`${task.id} ${task.updated_at} ${task.title}\n`);
|
|
101
|
+
if (!page.tasks?.length) stdout.write('No tasks.\n');
|
|
102
|
+
} else if (command === 'facts' || command === 'stream') {
|
|
103
|
+
for (const f of page.facts || []) {
|
|
104
|
+
const payload = Array.from(JSON.stringify(f.payload, (_key, value) => typeof value === 'bigint' ? JSON.rawJSON(value.toString()) : value));
|
|
105
|
+
const text = payload.length > 120 ? payload.slice(0, 117).join('') + '...' : payload.join('');
|
|
106
|
+
stdout.write(`${String(f.seq).padEnd(8)} ${f.created_at || ''} ${f.task_id ? f.task_id + ' ' : ''}${(f.type || '').padEnd(20)} ${f.actor_kind || ''}:${f.actor_id || ''} ${text}\n`);
|
|
107
|
+
}
|
|
108
|
+
stdout.write(`next_after_seq=${page.next_after_seq || 0}${command === 'stream' ? ` max_seq=${page.max_seq || 0}` : ''}\n`);
|
|
109
|
+
} else stdout.write(pretty(data) + '\n');
|
|
110
|
+
}
|
|
111
|
+
function pretty(raw) {
|
|
112
|
+
const tokens = raw.match(/"(?:\\.|[^"\\])*"|[{}\[\],:]|[^\s{}\[\],:]+/g) || [];
|
|
113
|
+
let level = 0, out = '';
|
|
114
|
+
const newline = () => '\n' + ' '.repeat(level);
|
|
115
|
+
tokens.forEach((token, i) => {
|
|
116
|
+
if (token === '{' || token === '[') { out += token; level++; if (tokens[i + 1] !== (token === '{' ? '}' : ']')) out += newline(); }
|
|
117
|
+
else if (token === '}' || token === ']') { level--; if (tokens[i - 1] !== (token === '}' ? '{' : '[')) out += newline(); out += token; }
|
|
118
|
+
else out += token === ',' ? ',' + newline() : token === ':' ? ': ' : token;
|
|
119
|
+
});
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
export async function run(args, { env = process.env, stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
123
|
+
if (args[0] === '--cli-version') {
|
|
124
|
+
let version = 'workspace';
|
|
125
|
+
try { version = JSON.parse(await readFile(new URL('./release.json', import.meta.url), 'utf8')).version; } catch {}
|
|
126
|
+
const report = { name: 'df-task', version, entry: fileURLToPath(import.meta.url) };
|
|
127
|
+
stdout.write((args.includes('--json') ? JSON.stringify(report) : `df-task ${version}`) + '\n');
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
let o, base, req;
|
|
131
|
+
try {
|
|
132
|
+
o = parse(args);
|
|
133
|
+
if (o.command === 'help' && !o.args.length) { stdout.write(help); return 0; }
|
|
134
|
+
if (o.help) {
|
|
135
|
+
const spec = Object.hasOwn(commands, o.command) ? commands[o.command] : undefined; if (!spec) throw Error(`unknown command ${o.command}`);
|
|
136
|
+
stdout.write(`df-task ${spec.usage} [--base-url <url>] [--json]\n\n${spec.description}\n`); return 0;
|
|
137
|
+
}
|
|
138
|
+
validate(o, env);
|
|
139
|
+
base = o.values['--base-url'] || env.DF_TASK_BASE_URL || 'http://127.0.0.1:8090';
|
|
140
|
+
const url = new URL(base);
|
|
141
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.search || url.hash || url.username || url.password) throw Error('invalid base URL');
|
|
142
|
+
req = await requestFor(o);
|
|
143
|
+
} catch (error) { stderr.write(error.message + '\n'); return 2; }
|
|
144
|
+
try {
|
|
145
|
+
const headers = req.body ? { 'Content-Type': 'application/json' } : {};
|
|
146
|
+
if (req.method !== 'GET') { headers['X-DF-Actor-Kind'] = env.DF_TASK_ACTOR_KIND; headers['X-DF-Actor-Id'] = env.DF_TASK_ACTOR_ID; }
|
|
147
|
+
const response = await fetch(base.replace(/\/+$/, '') + req.path, { method: req.method, body: req.body, headers, signal: AbortSignal.timeout(30000) });
|
|
148
|
+
const data = await response.text();
|
|
149
|
+
if (response.status >= 400) throw Error(`HTTP ${response.status}: ${data.trim()}`);
|
|
150
|
+
try { JSON.parse(data); } catch { throw Error('server returned invalid JSON'); }
|
|
151
|
+
if (o.json) stdout.write(data + '\n'); else human(o.command, data, stdout);
|
|
152
|
+
return 0;
|
|
153
|
+
} catch (error) { stderr.write(error.message + '\n'); return 1; }
|
|
154
|
+
}
|
|
155
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) process.exitCode = await run(process.argv.slice(2));
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Generated by generate-protocol.mjs from internal/model/fact.go.
|
|
2
|
+
export const factTypes = ["task_lifecycle","user_message","external_result","artifact_published","summary","help_requested","run_outcome","dispatch_intent","waiting_reason","handoff_to_human","adjudication","run_terminated","invocation_started","invocation_accepted","invocation_ended","attempt_failed"];
|
|
3
|
+
export const actorKinds = ["human","external","task_run","scheduler_run","driver"];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"0.3.0","project":"deepflow-task","sourceCommit":"3a8f5c8915cf5df74236eb4da693ac75f4c816a8","sourceDirty":false}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const commands = { health: '存活探针:进程还在吗', ready: '就绪探针:能不能干活(未就绪时退出码 1)', locks: '当前哪些 Task 正在被处理、被谁', task: '这个 Task 为什么不动', metrics: '抓取指标,可按子串过滤', help: '打印这段用法' };
|
|
7
|
+
export function displayWidth(text) {
|
|
8
|
+
return Array.from(text).reduce((width, char) => { const r = char.codePointAt(0); return width + ((r >= 0x1100 && r <= 0x115f) || (r >= 0x2e80 && r <= 0xa4cf) || (r >= 0xac00 && r <= 0xd7a3) || (r >= 0xf900 && r <= 0xfaff) || (r >= 0xfe30 && r <= 0xfe4f) || (r >= 0xff00 && r <= 0xff60) || (r >= 0xffe0 && r <= 0xffe6) ? 2 : 1); }, 0);
|
|
9
|
+
}
|
|
10
|
+
export const pad = (text, width) => text + ' '.repeat(Math.max(1, width - displayWidth(text)));
|
|
11
|
+
function usage(out) {
|
|
12
|
+
out.write('df-taskdriver —— deepflow-task-driver 运维 CLI\n\n用法: df-taskdriver [选项] <子命令> [参数]\n\n子命令:\n');
|
|
13
|
+
for (const [name, description] of Object.entries(commands)) out.write(` ${pad(name === 'task' ? 'task <task-id> [limit]' : name === 'metrics' ? 'metrics [过滤子串]' : name, 26)} ${description}\n`);
|
|
14
|
+
out.write('\n选项:\n --base-url string driver 实例的 ops 地址\n --timeout duration 单次请求超时 (default 10s)\n --json 原样输出实例返回的 JSON\n\n退出码: 0 肯定 / 1 实例给出否定回答 / 2 连不上或用法错误\n');
|
|
15
|
+
}
|
|
16
|
+
function duration(raw) {
|
|
17
|
+
if (raw === '0') return 0;
|
|
18
|
+
let total = 0, consumed = '';
|
|
19
|
+
for (const match of raw.matchAll(/(\d+(?:\.\d+)?)(ns|us|µs|μs|ms|s|m|h)/g)) {
|
|
20
|
+
consumed += match[0]; total += Number(match[1]) * { ns: 1e-6, us: 1e-3, 'µs': 1e-3, 'μs': 1e-3, ms: 1, s: 1000, m: 60000, h: 3600000 }[match[2]];
|
|
21
|
+
}
|
|
22
|
+
if (!consumed || consumed !== raw || !Number.isFinite(total)) throw Error(`invalid duration ${raw}`);
|
|
23
|
+
return total;
|
|
24
|
+
}
|
|
25
|
+
export function parse(args, env) {
|
|
26
|
+
const options = { args: [], base: env.DF_TASKDRIVER_BASE_URL || 'http://127.0.0.1:8081', timeout: 10000, json: false };
|
|
27
|
+
for (let i = 0; i < args.length; i++) {
|
|
28
|
+
if (!args[i].startsWith('-') || args[i] === '-') { options.args.push(args[i]); continue; }
|
|
29
|
+
if (args[i] === '--') { options.args.push(...args.slice(i + 1)); break; }
|
|
30
|
+
const [key, inline] = args[i].replace(/^-{1,2}/, '').split(/=(.*)/s);
|
|
31
|
+
if (key === 'help' || key === 'h') { options.help = true; break; }
|
|
32
|
+
if (key === 'json') { if (inline !== undefined && !['true', 'false'].includes(inline)) throw Error('invalid boolean value'); options.json = inline !== 'false'; continue; }
|
|
33
|
+
if (!['base-url', 'timeout'].includes(key)) throw Error(`flag provided but not defined: -${key}`);
|
|
34
|
+
const value = inline ?? args[++i]; if (value === undefined) throw Error(`flag needs an argument: -${key}`);
|
|
35
|
+
if (key === 'base-url') options.base = value; else options.timeout = duration(value);
|
|
36
|
+
}
|
|
37
|
+
return options;
|
|
38
|
+
}
|
|
39
|
+
const humanMS = (ms) => ms < 0 ? '未知' : `${(Number(ms || 0) / 1000).toFixed(1)}s`;
|
|
40
|
+
const compact = (payload) => Object.keys(payload || {}).sort().map((key) => `${key}=${typeof payload[key] === 'object' ? JSON.stringify(payload[key]) : payload[key]}`).join(' ') || '{}';
|
|
41
|
+
// Format tokens rather than re-encoding parsed numbers: seq values are int64.
|
|
42
|
+
function pretty(raw) {
|
|
43
|
+
const tokens = raw.match(/"(?:\\.|[^"\\])*"|[{}\[\],:]|[^\s{}\[\],:]+/g) || [];
|
|
44
|
+
let level = 0, out = '';
|
|
45
|
+
const newline = () => '\n' + ' '.repeat(level);
|
|
46
|
+
tokens.forEach((token, i) => {
|
|
47
|
+
if (token === '{' || token === '[') { out += token; level++; if (tokens[i + 1] !== (token === '{' ? '}' : ']')) out += newline(); }
|
|
48
|
+
else if (token === '}' || token === ']') { level--; if (tokens[i - 1] !== (token === '}' ? '{' : '[')) out += newline(); out += token; }
|
|
49
|
+
else out += token === ',' ? ',' + newline() : token === ':' ? ': ' : token;
|
|
50
|
+
});
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function human(command, body, status, out) {
|
|
54
|
+
const b = body || {};
|
|
55
|
+
if (command === 'health') out.write(`存活: ${b.status || ''} instance=${b.instance_id || ''}\n`);
|
|
56
|
+
if (command === 'ready') out.write(status === 200 ? '就绪\n' : `未就绪: ${b.error || ''}\n`);
|
|
57
|
+
if (command === 'locks') {
|
|
58
|
+
if (status !== 200) out.write(`读取失败: ${b.error || ''}\n`);
|
|
59
|
+
else if (!b.count) out.write('当前没有任何 Task 被持锁\n');
|
|
60
|
+
else {
|
|
61
|
+
out.write(`${b.count} 个 Task 正在被处理\n`);
|
|
62
|
+
for (const lock of b.locks || []) out.write(` ${lock.task_id} holder=${lock.holder} (${lock.recognized ? '本实例' : '其他实例'}) 剩余 ${humanMS(lock.remaining_ttl_ms)}\n`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (command === 'task') {
|
|
66
|
+
if (status !== 200) { out.write(`读取失败: ${b.error || ''}\n`); return; }
|
|
67
|
+
const d = b.decision || {};
|
|
68
|
+
out.write(`${b.task_id || ''}\n 结论: ${d.should_schedule ? '下一轮会调度' : '不会调度'} —— ${d.reason || ''}\n 事实: ${b.facts || 0} 条,max_seq=${b.max_seq || 0}\n`);
|
|
69
|
+
out.write(` 水位线: ${d.max_watermark || 0},最大触发事实: ${d.max_trigger_seq || 0},本轮锚点: ${d.anchor || 0}\n`);
|
|
70
|
+
out.write(` 失败预算: 组 ${d.budget_group || 0} 内 ${d.failures_in_budget_group || 0}/${d.failure_budget || 0} 次失败${d.budget_exhausted ? '(已耗尽,等人介入;发一条新的用户消息即可恢复)' : ''}\n`);
|
|
71
|
+
if (b.inflight?.length) {
|
|
72
|
+
out.write(' 在飞唤起:\n');
|
|
73
|
+
for (const inv of b.inflight) out.write(` ${inv.run_id} kind=${inv.run_kind} started_seq=${inv.started_seq || 0} intent_seq=${inv.intent_seq || 0} 水位线=${inv.watermark || 0} 锚点=${inv.anchor || 0}${inv.needs_replay ? ' [待重放]' : ''}\n`);
|
|
74
|
+
}
|
|
75
|
+
if (b.unfulfilled_intents?.length) {
|
|
76
|
+
out.write(' 未兑现意图:\n');
|
|
77
|
+
for (const intent of b.unfulfilled_intents) out.write(` seq=${intent.seq} ${compact(intent.payload)}\n`);
|
|
78
|
+
}
|
|
79
|
+
out.write(b.inflight?.length || b.unfulfilled_intents?.length ? ' 驱动这边还有未了之事(见上方在飞唤起/未兑现意图),先看它们卡在哪一步\n' : d.should_schedule ? ' 驱动这边没有未了之事,谓词已成立,下一拍就会起一轮调度——这一刻不用做什么\n' : ' 驱动无未了之事,继续查上游(事实是否写入、调度智能体是否下发)\n');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export async function run(args, { env = process.env, stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
83
|
+
if (args[0] === '--cli-version') {
|
|
84
|
+
let version = 'workspace';
|
|
85
|
+
try { version = JSON.parse(await readFile(new URL('./release.json', import.meta.url), 'utf8')).version; } catch {}
|
|
86
|
+
const report = { name: 'df-taskdriver', version, entry: fileURLToPath(import.meta.url) };
|
|
87
|
+
stdout.write((args.includes('--json') ? JSON.stringify(report) : `df-taskdriver ${version}`) + '\n');
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
let o, command, rest, path;
|
|
91
|
+
try {
|
|
92
|
+
o = parse(args, env); [command, ...rest] = o.args;
|
|
93
|
+
if (o.help || command === 'help') { usage(stdout); return 0; }
|
|
94
|
+
if (!command || !Object.hasOwn(commands, command)) { if (command) stderr.write(`未知子命令: ${command}\n\n`); usage(stderr); return 2; }
|
|
95
|
+
path = { health: '/healthz', ready: '/readyz', locks: '/debug/locks', metrics: '/metrics' }[command];
|
|
96
|
+
if (command === 'task') {
|
|
97
|
+
if (!rest.length) throw Error('用法: df-taskdriver task <task-id> [limit]');
|
|
98
|
+
if (rest[1] !== undefined && (!/^\+?\d+$/.test(rest[1]) || BigInt(rest[1]) < 1n || BigInt(rest[1]) > 9223372036854775807n)) throw Error(`limit 必须是正整数: ${rest[1]}`);
|
|
99
|
+
path = `/debug/tasks/${encodeURIComponent(rest[0])}${rest[1] !== undefined ? `?limit=${encodeURIComponent(rest[1])}` : ''}`;
|
|
100
|
+
}
|
|
101
|
+
} catch (error) { stderr.write(`错误: ${error.message}\n`); return 2; }
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
const stop = () => controller.abort();
|
|
104
|
+
process.once('SIGINT', stop); process.once('SIGTERM', stop);
|
|
105
|
+
const timer = o.timeout ? setTimeout(stop, o.timeout) : undefined;
|
|
106
|
+
try {
|
|
107
|
+
const response = await fetch(o.base.replace(/\/+$/, '') + path, { signal: controller.signal });
|
|
108
|
+
const chunks = []; let length = 0;
|
|
109
|
+
for await (const chunk of response.body) { length += chunk.length; if (length > 8 * 1024 * 1024) throw Error('response exceeds 8 MiB'); chunks.push(chunk); }
|
|
110
|
+
const data = Buffer.concat(chunks).toString('utf8'), affirmative = response.status >= 200 && response.status < 300;
|
|
111
|
+
let body, valid = true;
|
|
112
|
+
if (command !== 'metrics') { try { body = JSON.parse(data, (_key, value, context) => typeof value === 'number' && !Number.isSafeInteger(value) && /^-?\d+$/.test(context.source) ? BigInt(context.source) : value); } catch { valid = false; } }
|
|
113
|
+
if (command === 'metrics') {
|
|
114
|
+
stdout.write(rest.length ? data.split('\n').filter((line) => line.includes(rest[0])).join('\n') + '\n' : data);
|
|
115
|
+
return affirmative ? 0 : 1;
|
|
116
|
+
}
|
|
117
|
+
if (o.json) {
|
|
118
|
+
// Raw server JSON preserves unknown fields and integer precision.
|
|
119
|
+
stdout.write(valid ? pretty(data) + '\n' : data + (data.endsWith('\n') ? '' : '\n'));
|
|
120
|
+
return affirmative ? valid ? 0 : 2 : 1;
|
|
121
|
+
}
|
|
122
|
+
if (!valid || (body !== null && (typeof body !== 'object' || Array.isArray(body)))) {
|
|
123
|
+
stderr.write(`错误: 解析 ${path} 的响应失败(HTTP ${response.status},响应体前 200 字节: ${Array.from(data.trim()).slice(0, 200).join('')})\n`);
|
|
124
|
+
return affirmative ? 2 : 1;
|
|
125
|
+
}
|
|
126
|
+
human(command, body, response.status, stdout);
|
|
127
|
+
return command === 'ready' ? response.status === 200 ? 0 : 1 : affirmative ? 0 : 1;
|
|
128
|
+
} catch (error) { stderr.write(`错误: 请求 ${o.base} 失败: ${error.message}(检查 --base-url / DF_TASKDRIVER_BASE_URL)\n`); return 2; }
|
|
129
|
+
finally { clearTimeout(timer); process.removeListener('SIGINT', stop); process.removeListener('SIGTERM', stop); }
|
|
130
|
+
}
|
|
131
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) process.exitCode = await run(process.argv.slice(2));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"0.3.0","project":"deepflow-task-driver","sourceCommit":"ab582505c8d60a99c8d2939c3d58b31803a8a229","sourceDirty":false}
|