@hivemagnus/ardent-studio 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/NOTICE +12 -0
- package/README.md +133 -2
- package/dist/agents/config-edit.d.ts +20 -0
- package/dist/agents/config-edit.js +75 -0
- package/dist/agents/directory.d.ts +27 -0
- package/dist/agents/directory.js +97 -0
- package/dist/agents/operations.d.ts +47 -0
- package/dist/agents/operations.js +93 -0
- package/dist/auth/links.d.ts +27 -0
- package/dist/auth/links.js +46 -0
- package/dist/auth/oathweave.d.ts +46 -0
- package/dist/auth/oathweave.js +123 -0
- package/dist/blobs/store.d.ts +45 -0
- package/dist/blobs/store.js +159 -0
- package/dist/clients/chattermost.d.ts +16 -0
- package/dist/clients/chattermost.js +36 -0
- package/dist/clients/hivemem.d.ts +85 -0
- package/dist/clients/hivemem.js +89 -0
- package/dist/clients/magnus.d.ts +851 -0
- package/dist/clients/magnus.js +541 -0
- package/dist/clients/service-token.d.ts +24 -0
- package/dist/clients/service-token.js +87 -0
- package/dist/clients/sse.d.ts +8 -0
- package/dist/clients/sse.js +42 -0
- package/dist/conductor/activity.d.ts +38 -0
- package/dist/conductor/activity.js +101 -0
- package/dist/conductor/addressing.d.ts +24 -0
- package/dist/conductor/addressing.js +42 -0
- package/dist/conductor/conductor.d.ts +225 -0
- package/dist/conductor/conductor.js +939 -0
- package/dist/conductor/follower.d.ts +48 -0
- package/dist/conductor/follower.js +154 -0
- package/dist/conductor/live.d.ts +23 -0
- package/dist/conductor/live.js +32 -0
- package/dist/conductor/prompting.d.ts +74 -0
- package/dist/conductor/prompting.js +177 -0
- package/dist/config.d.ts +52 -0
- package/dist/config.js +163 -0
- package/dist/db/database.d.ts +12 -0
- package/dist/db/database.js +62 -0
- package/dist/db/json.d.ts +8 -0
- package/dist/db/json.js +13 -0
- package/dist/db/migrations.d.ts +6 -0
- package/dist/db/migrations.js +571 -0
- package/dist/db/repos/agents.d.ts +86 -0
- package/dist/db/repos/agents.js +92 -0
- package/dist/db/repos/audit.d.ts +37 -0
- package/dist/db/repos/audit.js +20 -0
- package/dist/db/repos/events.d.ts +29 -0
- package/dist/db/repos/events.js +55 -0
- package/dist/db/repos/files.d.ts +41 -0
- package/dist/db/repos/files.js +72 -0
- package/dist/db/repos/idempotency.d.ts +27 -0
- package/dist/db/repos/idempotency.js +46 -0
- package/dist/db/repos/links.d.ts +47 -0
- package/dist/db/repos/links.js +99 -0
- package/dist/db/repos/projects.d.ts +44 -0
- package/dist/db/repos/projects.js +107 -0
- package/dist/db/repos/proposals.d.ts +46 -0
- package/dist/db/repos/proposals.js +65 -0
- package/dist/db/repos/review.d.ts +191 -0
- package/dist/db/repos/review.js +195 -0
- package/dist/db/repos/room.d.ts +163 -0
- package/dist/db/repos/room.js +151 -0
- package/dist/db/repos/sessions.d.ts +36 -0
- package/dist/db/repos/sessions.js +30 -0
- package/dist/db/repos/summaries.d.ts +22 -0
- package/dist/db/repos/summaries.js +105 -0
- package/dist/db/repos/tasks.d.ts +103 -0
- package/dist/db/repos/tasks.js +112 -0
- package/dist/db/repos/team.d.ts +49 -0
- package/dist/db/repos/team.js +95 -0
- package/dist/db/repos/templates.d.ts +55 -0
- package/dist/db/repos/templates.js +120 -0
- package/dist/db/store.d.ts +57 -0
- package/dist/db/store.js +108 -0
- package/dist/events/bus.d.ts +21 -0
- package/dist/events/bus.js +31 -0
- package/dist/events/types.d.ts +3 -0
- package/dist/events/types.js +1 -0
- package/dist/ids.d.ts +28 -0
- package/dist/ids.js +81 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +17 -0
- package/dist/knowledge/extract.d.ts +17 -0
- package/dist/knowledge/extract.js +82 -0
- package/dist/knowledge/sync.d.ts +40 -0
- package/dist/knowledge/sync.js +172 -0
- package/dist/logger.d.ts +5 -0
- package/dist/logger.js +11 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +42 -0
- package/dist/review/anchors.d.ts +16 -0
- package/dist/review/anchors.js +67 -0
- package/dist/review/context.d.ts +4 -0
- package/dist/review/context.js +29 -0
- package/dist/review/docx.d.ts +29 -0
- package/dist/review/docx.js +21 -0
- package/dist/review/export.d.ts +16 -0
- package/dist/review/export.js +84 -0
- package/dist/review/importer.d.ts +68 -0
- package/dist/review/importer.js +215 -0
- package/dist/review/index.d.ts +13 -0
- package/dist/review/index.js +7 -0
- package/dist/server/app.d.ts +5 -0
- package/dist/server/app.js +44 -0
- package/dist/server/deps.d.ts +43 -0
- package/dist/server/deps.js +17 -0
- package/dist/server/env.d.ts +6 -0
- package/dist/server/env.js +1 -0
- package/dist/server/errors.d.ts +12 -0
- package/dist/server/errors.js +25 -0
- package/dist/server/routes/agent-studio.d.ts +5 -0
- package/dist/server/routes/agent-studio.js +441 -0
- package/dist/server/routes/agents.d.ts +3 -0
- package/dist/server/routes/agents.js +4 -0
- package/dist/server/routes/files.d.ts +3 -0
- package/dist/server/routes/files.js +88 -0
- package/dist/server/routes/health.d.ts +3 -0
- package/dist/server/routes/health.js +31 -0
- package/dist/server/routes/links.d.ts +3 -0
- package/dist/server/routes/links.js +79 -0
- package/dist/server/routes/projects.d.ts +3 -0
- package/dist/server/routes/projects.js +119 -0
- package/dist/server/routes/review.d.ts +3 -0
- package/dist/server/routes/review.js +198 -0
- package/dist/server/routes/room.d.ts +9 -0
- package/dist/server/routes/room.js +224 -0
- package/dist/server/routes/stream.d.ts +4 -0
- package/dist/server/routes/stream.js +104 -0
- package/dist/server/routes/team.d.ts +3 -0
- package/dist/server/routes/team.js +183 -0
- package/dist/server/routes/templates.d.ts +11 -0
- package/dist/server/routes/templates.js +103 -0
- package/dist/server/routes/work.d.ts +3 -0
- package/dist/server/routes/work.js +139 -0
- package/dist/server/routing.d.ts +31 -0
- package/dist/server/routing.js +132 -0
- package/dist/service.d.ts +34 -0
- package/dist/service.js +93 -0
- package/dist/shared/browser.d.ts +5824 -0
- package/dist/shared/browser.js +1318 -0
- package/dist/team/proposals.d.ts +129 -0
- package/dist/team/proposals.js +349 -0
- package/dist/team/service.d.ts +12 -0
- package/dist/team/service.js +13 -0
- package/dist/team/validation.d.ts +37 -0
- package/dist/team/validation.js +157 -0
- package/dist/templates/secrets.d.ts +2 -0
- package/dist/templates/secrets.js +23 -0
- package/dist/templates/seeds.d.ts +34 -0
- package/dist/templates/seeds.js +277 -0
- package/dist/url.d.ts +2 -0
- package/dist/url.js +7 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +11 -0
- package/docs/openapi-browser.json +12658 -0
- package/package.json +91 -4
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ardentTaskDoneEventSchema, type ArdentTaskEvent, type MagnusClient } from '../clients/magnus.js';
|
|
3
|
+
import type { Logger } from '../logger.js';
|
|
4
|
+
export type FollowerMagnus = Pick<MagnusClient, 'streamTask' | 'taskEvents' | 'task'>;
|
|
5
|
+
export type DoneRecord = z.infer<typeof ardentTaskDoneEventSchema>;
|
|
6
|
+
/** Ardent states after which a task's turn is over. Ardent keeps `input_required` out of its stream's done set, so it is listed here. */
|
|
7
|
+
export declare const ARDENT_FINISHED: readonly ["completed", "partial", "failed", "cancelled", "unknown", "input_required"];
|
|
8
|
+
export interface FollowerHooks {
|
|
9
|
+
/** The journal sequence already handled. */
|
|
10
|
+
cursor(): number;
|
|
11
|
+
text(step: number, text: string, mode: 'replace' | 'append'): void;
|
|
12
|
+
/** Handle and persist one journal event. Returns true when the event ends the turn. */
|
|
13
|
+
event(event: ArdentTaskEvent): Promise<boolean> | boolean;
|
|
14
|
+
done(record: DoneRecord): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export interface FollowerOptions {
|
|
17
|
+
pollMinMs?: number;
|
|
18
|
+
pollMaxMs?: number;
|
|
19
|
+
/** How long to poll before trying the stream again. */
|
|
20
|
+
pollWindowMs?: number;
|
|
21
|
+
/** Pause between a clean stream end and the reconnect. */
|
|
22
|
+
reconnectMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare class TaskFollower {
|
|
25
|
+
private readonly magnus;
|
|
26
|
+
private readonly agentRef;
|
|
27
|
+
private readonly ardentTaskId;
|
|
28
|
+
private readonly hooks;
|
|
29
|
+
private readonly logger;
|
|
30
|
+
private readonly abort;
|
|
31
|
+
private running;
|
|
32
|
+
private readonly pollMin;
|
|
33
|
+
private readonly pollMax;
|
|
34
|
+
private readonly pollWindow;
|
|
35
|
+
private readonly reconnect;
|
|
36
|
+
/** How the follower currently reads the task: for tests and diagnostics. */
|
|
37
|
+
mode: 'stream' | 'poll' | 'done';
|
|
38
|
+
constructor(magnus: FollowerMagnus, agentRef: string, ardentTaskId: string, hooks: FollowerHooks, logger: Logger, options?: FollowerOptions);
|
|
39
|
+
start(): Promise<void>;
|
|
40
|
+
stop(): Promise<void>;
|
|
41
|
+
private get stopped();
|
|
42
|
+
private loop;
|
|
43
|
+
/** One SSE subscription. True when the task finished. */
|
|
44
|
+
private stream;
|
|
45
|
+
/** Poll the journal and the task. True when the task finished; false to try the stream again. */
|
|
46
|
+
private poll;
|
|
47
|
+
private sleep;
|
|
48
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Follows one Ardent task through Magnus: one SSE subscription (journal events and live reply
|
|
2
|
+
// text) resumed after the last handled journal sequence. Ardent ends a stream after 60 seconds,
|
|
3
|
+
// so a clean end reconnects. When the stream fails, the follower polls the journal and the task
|
|
4
|
+
// every 1 to 5 seconds, then tries the stream again.
|
|
5
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
|
+
import { ardentTaskDoneEventSchema, ardentTaskEventSchema, ardentTaskTextEventSchema, MagnusError } from '../clients/magnus.js';
|
|
7
|
+
/** Ardent states after which a task's turn is over. Ardent keeps `input_required` out of its stream's done set, so it is listed here. */
|
|
8
|
+
export const ARDENT_FINISHED = ['completed', 'partial', 'failed', 'cancelled', 'unknown', 'input_required'];
|
|
9
|
+
export class TaskFollower {
|
|
10
|
+
magnus;
|
|
11
|
+
agentRef;
|
|
12
|
+
ardentTaskId;
|
|
13
|
+
hooks;
|
|
14
|
+
logger;
|
|
15
|
+
abort = new AbortController();
|
|
16
|
+
running;
|
|
17
|
+
pollMin;
|
|
18
|
+
pollMax;
|
|
19
|
+
pollWindow;
|
|
20
|
+
reconnect;
|
|
21
|
+
/** How the follower currently reads the task: for tests and diagnostics. */
|
|
22
|
+
mode = 'stream';
|
|
23
|
+
constructor(magnus, agentRef, ardentTaskId, hooks, logger, options = {}) {
|
|
24
|
+
this.magnus = magnus;
|
|
25
|
+
this.agentRef = agentRef;
|
|
26
|
+
this.ardentTaskId = ardentTaskId;
|
|
27
|
+
this.hooks = hooks;
|
|
28
|
+
this.logger = logger;
|
|
29
|
+
this.pollMin = options.pollMinMs ?? 1_000;
|
|
30
|
+
this.pollMax = options.pollMaxMs ?? 5_000;
|
|
31
|
+
this.pollWindow = options.pollWindowMs ?? 30_000;
|
|
32
|
+
this.reconnect = options.reconnectMs ?? 250;
|
|
33
|
+
}
|
|
34
|
+
start() {
|
|
35
|
+
this.running ??= this.loop().catch(error => {
|
|
36
|
+
this.logger.error('Task follower stopped unexpectedly', { task_id: this.ardentTaskId, error: error instanceof Error ? error.message : 'unknown' });
|
|
37
|
+
});
|
|
38
|
+
return this.running;
|
|
39
|
+
}
|
|
40
|
+
async stop() {
|
|
41
|
+
this.abort.abort();
|
|
42
|
+
await this.running;
|
|
43
|
+
}
|
|
44
|
+
get stopped() { return this.abort.signal.aborted; }
|
|
45
|
+
async loop() {
|
|
46
|
+
while (!this.stopped) {
|
|
47
|
+
this.mode = 'stream';
|
|
48
|
+
let failed = false;
|
|
49
|
+
try {
|
|
50
|
+
if (await this.stream()) {
|
|
51
|
+
this.mode = 'done';
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (this.stopped)
|
|
57
|
+
return;
|
|
58
|
+
failed = true;
|
|
59
|
+
this.logger.warn('Task stream failed; polling', { task_id: this.ardentTaskId, error: error instanceof Error ? error.message.slice(0, 200) : 'unknown' });
|
|
60
|
+
}
|
|
61
|
+
if (this.stopped)
|
|
62
|
+
return;
|
|
63
|
+
if (failed) {
|
|
64
|
+
this.mode = 'poll';
|
|
65
|
+
if (await this.poll()) {
|
|
66
|
+
this.mode = 'done';
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
await this.sleep(this.reconnect);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** One SSE subscription. True when the task finished. */
|
|
76
|
+
async stream() {
|
|
77
|
+
for await (const message of this.magnus.streamTask(this.agentRef, this.ardentTaskId, { after: this.hooks.cursor(), signal: this.abort.signal })) {
|
|
78
|
+
if (this.stopped)
|
|
79
|
+
return false;
|
|
80
|
+
if (message.event === 'task.text' || message.event === 'task.delta') {
|
|
81
|
+
const parsed = ardentTaskTextEventSchema.safeParse(parseJson(message.data));
|
|
82
|
+
if (parsed.success)
|
|
83
|
+
this.hooks.text(parsed.data.step, parsed.data.text, message.event === 'task.text' ? 'replace' : 'append');
|
|
84
|
+
}
|
|
85
|
+
else if (message.event === 'task.event') {
|
|
86
|
+
const parsed = ardentTaskEventSchema.safeParse(parseJson(message.data));
|
|
87
|
+
if (!parsed.success)
|
|
88
|
+
continue;
|
|
89
|
+
if (await this.hooks.event(parsed.data))
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
else if (message.event === 'task.done') {
|
|
93
|
+
const parsed = ardentTaskDoneEventSchema.safeParse(parseJson(message.data));
|
|
94
|
+
await this.hooks.done(parsed.success ? parsed.data : { id: this.ardentTaskId, state: 'unknown', error: 'Ardent sent an unreadable final task record' });
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
/** Poll the journal and the task. True when the task finished; false to try the stream again. */
|
|
101
|
+
async poll() {
|
|
102
|
+
const until = Date.now() + this.pollWindow;
|
|
103
|
+
let interval = this.pollMin;
|
|
104
|
+
while (!this.stopped && Date.now() < until) {
|
|
105
|
+
try {
|
|
106
|
+
// A journal page Magnus cannot relay (an error, or a page over its size limit) must not
|
|
107
|
+
// hide that the task finished: the task record is read either way.
|
|
108
|
+
let events = [];
|
|
109
|
+
let unreadable;
|
|
110
|
+
try {
|
|
111
|
+
events = await this.magnus.taskEvents(this.agentRef, this.ardentTaskId, this.hooks.cursor(), { signal: this.abort.signal });
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error instanceof MagnusError && error.status === 404)
|
|
115
|
+
throw error;
|
|
116
|
+
unreadable = error;
|
|
117
|
+
}
|
|
118
|
+
for (const event of events)
|
|
119
|
+
if (await this.hooks.event(event))
|
|
120
|
+
return true;
|
|
121
|
+
const task = await this.magnus.task(this.agentRef, this.ardentTaskId, { signal: this.abort.signal });
|
|
122
|
+
if (ARDENT_FINISHED.includes(task.state)) {
|
|
123
|
+
await this.hooks.done(task);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (unreadable !== undefined)
|
|
127
|
+
throw unreadable;
|
|
128
|
+
interval = events.length ? this.pollMin : Math.min(this.pollMax, interval * 2);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (this.stopped)
|
|
132
|
+
return false;
|
|
133
|
+
if (error instanceof MagnusError && error.status === 404) {
|
|
134
|
+
await this.hooks.done({ id: this.ardentTaskId, state: 'unknown', error: 'Ardent has no record of this task' });
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
interval = this.pollMax;
|
|
138
|
+
}
|
|
139
|
+
await this.sleep(interval);
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
async sleep(ms) {
|
|
144
|
+
await delay(ms, undefined, { signal: this.abort.signal }).catch(() => undefined);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function parseJson(text) {
|
|
148
|
+
try {
|
|
149
|
+
return JSON.parse(text);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EventBus } from '../events/bus.js';
|
|
2
|
+
export interface LiveEntry {
|
|
3
|
+
projectId: string;
|
|
4
|
+
step: number;
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class LiveText {
|
|
8
|
+
private readonly bus;
|
|
9
|
+
private readonly entries;
|
|
10
|
+
constructor(bus: EventBus);
|
|
11
|
+
/** A step started or a stream (re)connected: the text is the whole current step. */
|
|
12
|
+
replace(projectId: string, messageId: string, step: number, text: string): void;
|
|
13
|
+
/** More text for the current step. A delta for another step starts that step. */
|
|
14
|
+
append(projectId: string, messageId: string, step: number, text: string): void;
|
|
15
|
+
get(messageId: string): LiveEntry | undefined;
|
|
16
|
+
clear(messageId: string): void;
|
|
17
|
+
/** Current text of every streaming message in a project. */
|
|
18
|
+
snapshot(projectId: string): Array<{
|
|
19
|
+
message_id: string;
|
|
20
|
+
step: number;
|
|
21
|
+
text: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export class LiveText {
|
|
2
|
+
bus;
|
|
3
|
+
entries = new Map();
|
|
4
|
+
constructor(bus) {
|
|
5
|
+
this.bus = bus;
|
|
6
|
+
}
|
|
7
|
+
/** A step started or a stream (re)connected: the text is the whole current step. */
|
|
8
|
+
replace(projectId, messageId, step, text) {
|
|
9
|
+
this.entries.set(messageId, { projectId, step, text });
|
|
10
|
+
this.bus.publishEphemeral(projectId, 'message.delta', { message_id: messageId, text, mode: 'replace', step });
|
|
11
|
+
}
|
|
12
|
+
/** More text for the current step. A delta for another step starts that step. */
|
|
13
|
+
append(projectId, messageId, step, text) {
|
|
14
|
+
const current = this.entries.get(messageId);
|
|
15
|
+
if (current && current.step !== step) {
|
|
16
|
+
this.replace(projectId, messageId, step, text);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
this.entries.set(messageId, { projectId, step, text: (current?.text ?? '') + text });
|
|
20
|
+
this.bus.publishEphemeral(projectId, 'message.delta', { message_id: messageId, text, mode: 'append', step });
|
|
21
|
+
}
|
|
22
|
+
get(messageId) {
|
|
23
|
+
return this.entries.get(messageId);
|
|
24
|
+
}
|
|
25
|
+
clear(messageId) {
|
|
26
|
+
this.entries.delete(messageId);
|
|
27
|
+
}
|
|
28
|
+
/** Current text of every streaming message in a project. */
|
|
29
|
+
snapshot(projectId) {
|
|
30
|
+
return [...this.entries].filter(([, entry]) => entry.projectId === projectId).map(([id, entry]) => ({ message_id: id, step: entry.step, text: entry.text }));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ArdentContextItem, ArdentTeamMission } from '../clients/magnus.js';
|
|
2
|
+
import type { Project } from '../db/repos/projects.js';
|
|
3
|
+
import type { Message } from '../db/repos/room.js';
|
|
4
|
+
import type { StudioTask } from '../db/repos/tasks.js';
|
|
5
|
+
import type { TeamBody } from '../db/repos/team.js';
|
|
6
|
+
export type TeamRole = TeamBody['roles'][number];
|
|
7
|
+
/** Ardent's limits: a mission objective and a task prompt are at most 16000 characters. */
|
|
8
|
+
export declare const PROMPT_LIMIT = 16000;
|
|
9
|
+
export interface AttachmentText {
|
|
10
|
+
name: string;
|
|
11
|
+
mime: string;
|
|
12
|
+
text: string | null;
|
|
13
|
+
}
|
|
14
|
+
export interface TurnInput {
|
|
15
|
+
project: Pick<Project, 'id' | 'name' | 'brief_md' | 'audience' | 'constraints' | 'deliverables'>;
|
|
16
|
+
team: Pick<TeamBody, 'roles' | 'relationships'>;
|
|
17
|
+
role: TeamRole;
|
|
18
|
+
task: Pick<StudioTask, 'id' | 'mode' | 'title' | 'instructions_md' | 'source'>;
|
|
19
|
+
/** No earlier turn in this Ardent session (or no session at all): include the brief and roster. */
|
|
20
|
+
firstTurn: boolean;
|
|
21
|
+
/** Room messages the agent has not been shown, oldest first, without the trigger. */
|
|
22
|
+
unseen: readonly Message[];
|
|
23
|
+
/** The message this turn answers, when there is one. */
|
|
24
|
+
trigger: Message | undefined;
|
|
25
|
+
/** Readable names for message authors and addressees. */
|
|
26
|
+
names: {
|
|
27
|
+
author: (message: Message) => string;
|
|
28
|
+
role: (key: string) => string;
|
|
29
|
+
};
|
|
30
|
+
attachments: readonly AttachmentText[];
|
|
31
|
+
discussion?: {
|
|
32
|
+
question: string;
|
|
33
|
+
desired_output: string;
|
|
34
|
+
round: number;
|
|
35
|
+
max_rounds: number;
|
|
36
|
+
summary: boolean;
|
|
37
|
+
transcript: readonly Message[];
|
|
38
|
+
} | undefined;
|
|
39
|
+
/** Further material for this turn, such as the review comments of a revision request. */
|
|
40
|
+
extra?: ReadonlyArray<{
|
|
41
|
+
title: string;
|
|
42
|
+
text: string;
|
|
43
|
+
mime?: string;
|
|
44
|
+
}> | undefined;
|
|
45
|
+
}
|
|
46
|
+
export interface ComposedTurn {
|
|
47
|
+
/** Mission objective (missions) — at most PROMPT_LIMIT characters. */
|
|
48
|
+
objective: string;
|
|
49
|
+
/** Large material for a mission, within Ardent's 64 KiB. */
|
|
50
|
+
contextItems: ArdentContextItem[];
|
|
51
|
+
/** The same turn as one prompt for task-submit, with large material shortened to fit. */
|
|
52
|
+
prompt: string;
|
|
53
|
+
}
|
|
54
|
+
/** Compose one turn. Large material becomes context items for a mission and is shortened for a plain prompt. */
|
|
55
|
+
export declare function composeTurn(input: TurnInput): ComposedTurn;
|
|
56
|
+
/** Mission tools a role may use. A hand-off tool only when the team gives the role someone to hand to. */
|
|
57
|
+
export declare function missionResources(input: {
|
|
58
|
+
team: Pick<TeamBody, 'relationships'>;
|
|
59
|
+
role: TeamRole;
|
|
60
|
+
hivememProjectId: string;
|
|
61
|
+
}): string[];
|
|
62
|
+
/** Relationship types along which Studio starts a requested hand-off. */
|
|
63
|
+
export declare const HANDOFF_EDGES: readonly string[];
|
|
64
|
+
/** The TeamMission for a Studio task: constraints from the project, criteria from the request. */
|
|
65
|
+
export declare function buildMission(input: {
|
|
66
|
+
task: Pick<StudioTask, 'id' | 'mode' | 'title' | 'assignment_revision'> & {
|
|
67
|
+
source?: Record<string, unknown>;
|
|
68
|
+
};
|
|
69
|
+
project: Pick<Project, 'name' | 'constraints' | 'deliverables'>;
|
|
70
|
+
team: Pick<TeamBody, 'relationships'>;
|
|
71
|
+
role: TeamRole;
|
|
72
|
+
hivememProjectId: string;
|
|
73
|
+
now: Date;
|
|
74
|
+
}): ArdentTeamMission;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { ARDENT_CONTEXT_ITEMS_MAX_BYTES } from '../clients/magnus.js';
|
|
2
|
+
/** Ardent's limits: a mission objective and a task prompt are at most 16000 characters. */
|
|
3
|
+
export const PROMPT_LIMIT = 16_000;
|
|
4
|
+
/** Inline sizes before material moves into context items. */
|
|
5
|
+
const INLINE_BRIEF = 3_000;
|
|
6
|
+
const INLINE_ROOM = 6_000;
|
|
7
|
+
const ITEM_TEXT_LIMIT = 24_000;
|
|
8
|
+
const clip = (text, max) => text.length <= max ? text : `${text.slice(0, Math.max(0, max - 15)).trimEnd()}\n[…shortened]`;
|
|
9
|
+
const bytes = (text) => Buffer.byteLength(text, 'utf8');
|
|
10
|
+
function renderMessage(message, names) {
|
|
11
|
+
const to = message.addressees.length ? ` → ${message.addressees.map(names.role).join(', ')}` : '';
|
|
12
|
+
return `[${names.author(message)}${to}] ${message.body_md.trim() || '(no text)'}`;
|
|
13
|
+
}
|
|
14
|
+
function roleSection(role) {
|
|
15
|
+
const lines = [`You are ${role.name} (role key "${role.key}")${role.coordinator ? ', the team coordinator' : ''}.`];
|
|
16
|
+
if (role.responsibilities_md.trim())
|
|
17
|
+
lines.push(`Responsibilities:\n${role.responsibilities_md.trim()}`);
|
|
18
|
+
if (role.instructions_md.trim())
|
|
19
|
+
lines.push(`Instructions for this project:\n${role.instructions_md.trim()}`);
|
|
20
|
+
if (role.contributions.length)
|
|
21
|
+
lines.push(`You contribute: ${role.contributions.join('; ')}.`);
|
|
22
|
+
if (role.deliverables.length)
|
|
23
|
+
lines.push(`You deliver: ${role.deliverables.join('; ')}.`);
|
|
24
|
+
return lines.join('\n');
|
|
25
|
+
}
|
|
26
|
+
function rosterSection(input) {
|
|
27
|
+
const lines = input.team.roles.map(role => `- ${role.name} ("${role.key}")${role.coordinator ? ', coordinator' : ''}${role.agent_ref ? '' : ' (no agent bound)'}`);
|
|
28
|
+
const edges = input.team.relationships.filter(edge => edge.from === input.role.key || edge.to === input.role.key)
|
|
29
|
+
.map(edge => `- ${input.names.role(edge.from)} ${edge.type.replace(/_/g, ' ')} ${input.names.role(edge.to)}`);
|
|
30
|
+
return [`Team:\n${lines.join('\n')}`, edges.length ? `Your working relationships:\n${edges.join('\n')}` : ''].filter(Boolean).join('\n');
|
|
31
|
+
}
|
|
32
|
+
function projectSection(input, brief) {
|
|
33
|
+
const project = input.project;
|
|
34
|
+
const lines = [`Project: ${project.name}`];
|
|
35
|
+
if (project.audience)
|
|
36
|
+
lines.push(`Audience: ${project.audience}`);
|
|
37
|
+
if (project.constraints.length)
|
|
38
|
+
lines.push(`Constraints:\n${project.constraints.map(item => `- ${item}`).join('\n')}`);
|
|
39
|
+
if (project.deliverables.length) {
|
|
40
|
+
lines.push(`Requested deliverables:\n${project.deliverables.map(item => `- ${item.name} (${item.formats.join(', ')})${item.producer_role_key ? ` by ${input.names.role(item.producer_role_key)}` : ''}`).join('\n')}`);
|
|
41
|
+
}
|
|
42
|
+
if (brief)
|
|
43
|
+
lines.push(`Brief:\n${brief}`);
|
|
44
|
+
return lines.join('\n');
|
|
45
|
+
}
|
|
46
|
+
function requestSection(input) {
|
|
47
|
+
const { task, trigger, names, discussion } = input;
|
|
48
|
+
if (discussion) {
|
|
49
|
+
const head = `Discussion question: ${discussion.question}\nDesired output: ${discussion.desired_output.replace(/_/g, ' ')}.`;
|
|
50
|
+
return discussion.summary
|
|
51
|
+
? `${head}\nThe discussion has ended. As coordinator, summarize it as the desired output. Name open disagreements; do not add positions nobody took.`
|
|
52
|
+
: `${head}\nRound ${discussion.round} of ${discussion.max_rounds}. Give your contribution from your role in a few short paragraphs, responding to what others said.`;
|
|
53
|
+
}
|
|
54
|
+
if (typeof task.source.answer_to === 'string' && trigger) {
|
|
55
|
+
const question = typeof task.source.question === 'string' ? task.source.question : 'your question';
|
|
56
|
+
return `${names.author(trigger)} answered ${JSON.stringify(question)}:\n${trigger.body_md.trim()}`;
|
|
57
|
+
}
|
|
58
|
+
if (task.mode === 'turn' && trigger)
|
|
59
|
+
return `Message for you from ${names.author(trigger)}:\n${trigger.body_md.trim()}`;
|
|
60
|
+
const lines = [`${task.mode === 'revision' ? 'Revision request' : task.mode === 'review' ? 'Review request' : 'Assignment'}: ${task.title}`];
|
|
61
|
+
if (task.instructions_md.trim())
|
|
62
|
+
lines.push(task.instructions_md.trim());
|
|
63
|
+
if (typeof task.source.handoff_from === 'string')
|
|
64
|
+
lines.push(`Handed off by ${names.role(task.source.handoff_from)}.`);
|
|
65
|
+
if (trigger)
|
|
66
|
+
lines.push(`From the message by ${names.author(trigger)}:\n${trigger.body_md.trim()}`);
|
|
67
|
+
return lines.join('\n');
|
|
68
|
+
}
|
|
69
|
+
/** Compose one turn. Large material becomes context items for a mission and is shortened for a plain prompt. */
|
|
70
|
+
export function composeTurn(input) {
|
|
71
|
+
const items = [];
|
|
72
|
+
let itemBytes = 0;
|
|
73
|
+
const addItem = (title, text, mime = 'text/markdown') => {
|
|
74
|
+
const body = clip(text, ITEM_TEXT_LIMIT);
|
|
75
|
+
const size = bytes(title + body);
|
|
76
|
+
if (!body.trim() || itemBytes + size > ARDENT_CONTEXT_ITEMS_MAX_BYTES || items.length >= 64)
|
|
77
|
+
return false;
|
|
78
|
+
items.push({ title, mime, text: body });
|
|
79
|
+
itemBytes += size;
|
|
80
|
+
return true;
|
|
81
|
+
};
|
|
82
|
+
const brief = input.firstTurn ? input.project.brief_md.trim() : '';
|
|
83
|
+
const briefInline = brief.length <= INLINE_BRIEF ? brief : '';
|
|
84
|
+
const briefItem = brief && !briefInline ? addItem('Project brief', brief) : false;
|
|
85
|
+
const transcript = input.discussion && !input.discussion.summary ? [] : input.unseen;
|
|
86
|
+
const room = transcript.map(message => renderMessage(message, input.names)).join('\n');
|
|
87
|
+
const roomInline = room.length <= INLINE_ROOM ? room : '';
|
|
88
|
+
const roomItem = room && !roomInline ? addItem('Room messages since your last turn', room) : false;
|
|
89
|
+
const discussionRoom = input.discussion ? input.discussion.transcript.map(message => renderMessage(message, input.names)).join('\n') : '';
|
|
90
|
+
const discussionInline = discussionRoom.length <= INLINE_ROOM ? discussionRoom : '';
|
|
91
|
+
const discussionItem = discussionRoom && !discussionInline ? addItem('Discussion so far', discussionRoom) : false;
|
|
92
|
+
const attachmentNotes = [];
|
|
93
|
+
for (const attachment of input.attachments) {
|
|
94
|
+
if (attachment.text !== null && addItem(`Attachment: ${attachment.name}`, attachment.text, attachment.mime.startsWith('text/') ? attachment.mime : 'text/plain')) {
|
|
95
|
+
attachmentNotes.push(`- ${attachment.name} (in context items)`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
attachmentNotes.push(`- ${attachment.name} (${attachment.mime}; not included as text)`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const extraNotes = [];
|
|
102
|
+
const extraInline = [];
|
|
103
|
+
for (const item of input.extra ?? []) {
|
|
104
|
+
if (addItem(item.title, item.text, item.mime))
|
|
105
|
+
extraNotes.push(`- ${item.title}`);
|
|
106
|
+
extraInline.push(`${item.title}:\n${clip(item.text, 3_000)}`);
|
|
107
|
+
}
|
|
108
|
+
const sections = (mode, budget) => [
|
|
109
|
+
roleSection(input.role),
|
|
110
|
+
input.firstTurn ? projectSection(input, budget.brief) : `Project: ${input.project.name}`,
|
|
111
|
+
input.firstTurn ? rosterSection(input) : '',
|
|
112
|
+
budget.room ? `Room messages since your last turn:\n${budget.room}` : '',
|
|
113
|
+
budget.discussion ? `Discussion so far:\n${budget.discussion}` : '',
|
|
114
|
+
attachmentNotes.length ? `Attachments:\n${attachmentNotes.join('\n')}` : '',
|
|
115
|
+
mode === 'mission' && extraNotes.length ? `In context items:\n${extraNotes.join('\n')}` : '',
|
|
116
|
+
mode === 'prompt' && extraInline.length ? extraInline.join('\n\n') : '',
|
|
117
|
+
requestSection(input),
|
|
118
|
+
mode === 'mission'
|
|
119
|
+
? 'Reply to the team in plain prose; your reply is posted in the room as your message.'
|
|
120
|
+
: 'Reply to the team in plain prose; your reply is posted in the room as your message. You cannot hand work to other roles or save files in this turn; say what you need instead.',
|
|
121
|
+
].filter(Boolean).join('\n\n');
|
|
122
|
+
const objective = clip(sections('mission', {
|
|
123
|
+
brief: briefInline || (briefItem ? '(in the context item "Project brief")' : clip(brief, INLINE_BRIEF)),
|
|
124
|
+
room: roomInline || (roomItem ? '(in the context item "Room messages since your last turn")' : clip(room.slice(-INLINE_ROOM), INLINE_ROOM)),
|
|
125
|
+
discussion: discussionInline || (discussionItem ? '(in the context item "Discussion so far")' : clip(discussionRoom.slice(-INLINE_ROOM), INLINE_ROOM)),
|
|
126
|
+
}), PROMPT_LIMIT);
|
|
127
|
+
// A plain prompt has no context items: keep the newest room text and the start of the brief.
|
|
128
|
+
const fixed = sections('prompt', { brief: '', room: '', discussion: '' }).length;
|
|
129
|
+
const spare = Math.max(0, PROMPT_LIMIT - fixed - 200);
|
|
130
|
+
const roomShare = Math.floor(spare * (brief ? 0.5 : 1));
|
|
131
|
+
const tail = (text, max) => text.length <= max ? text : `[…earlier messages left out]\n${text.slice(text.length - max + 32)}`;
|
|
132
|
+
const prompt = clip(sections('prompt', {
|
|
133
|
+
brief: brief ? clip(brief, Math.max(200, spare - Math.min(roomShare, room.length + discussionRoom.length))) : '',
|
|
134
|
+
room: room ? tail(room, Math.max(200, Math.floor(roomShare / (discussionRoom ? 2 : 1)))) : '',
|
|
135
|
+
discussion: discussionRoom ? tail(discussionRoom, Math.max(200, Math.floor(roomShare / 2))) : '',
|
|
136
|
+
}), PROMPT_LIMIT);
|
|
137
|
+
return { objective, contextItems: items, prompt };
|
|
138
|
+
}
|
|
139
|
+
/** Mission tools a role may use. A hand-off tool only when the team gives the role someone to hand to. */
|
|
140
|
+
export function missionResources(input) {
|
|
141
|
+
const resources = ['tool:team_ask_user', 'tool:team_propose_decision', 'tool:artifact_write', 'tool:document_render', 'tool:development_note'];
|
|
142
|
+
if (input.team.relationships.some(edge => edge.from === input.role.key && HANDOFF_EDGES.includes(edge.type)))
|
|
143
|
+
resources.unshift('tool:team_handoff');
|
|
144
|
+
resources.push(`project:${input.hivememProjectId}`);
|
|
145
|
+
return resources;
|
|
146
|
+
}
|
|
147
|
+
/** Relationship types along which Studio starts a requested hand-off. */
|
|
148
|
+
export const HANDOFF_EDGES = ['delegates_to', 'hands_off_to'];
|
|
149
|
+
const MISSION_HOURS = 6;
|
|
150
|
+
const missionLine = (text) => {
|
|
151
|
+
let line = text.replace(/\s+/g, ' ').trim();
|
|
152
|
+
while (bytes(line) > 1000)
|
|
153
|
+
line = line.slice(0, -16);
|
|
154
|
+
return line;
|
|
155
|
+
};
|
|
156
|
+
/** The TeamMission for a Studio task: constraints from the project, criteria from the request. */
|
|
157
|
+
export function buildMission(input) {
|
|
158
|
+
const { task, role, project } = input;
|
|
159
|
+
const unique = (values) => [...new Set(values.map(missionLine).filter(Boolean))].slice(0, 32);
|
|
160
|
+
const criteria = task.mode === 'turn' ? ['Reply to the latest message addressed to you, in your role']
|
|
161
|
+
: task.mode === 'discussion_turn' ? ['Contribute your view to the discussion round, in your role']
|
|
162
|
+
: [`Complete: ${task.title}`];
|
|
163
|
+
const expected = task.mode === 'revision' && typeof task.source?.deliverable_name === 'string' ? [task.source.deliverable_name]
|
|
164
|
+
: task.mode === 'assignment' ? project.deliverables.filter(item => item.producer_role_key === role.key).map(item => item.name)
|
|
165
|
+
: [];
|
|
166
|
+
return {
|
|
167
|
+
id: task.id,
|
|
168
|
+
revision: Math.max(1, task.assignment_revision ?? 1),
|
|
169
|
+
parent_task: null,
|
|
170
|
+
constraints: unique([`Project: ${project.name}`, `Role: ${role.name}`, ...project.constraints]),
|
|
171
|
+
allowed_actions: ['observe', 'analyze', 'create_artifact', 'create_draft'],
|
|
172
|
+
resources: missionResources(input),
|
|
173
|
+
deadline: Math.floor(input.now.getTime() / 1000) + MISSION_HOURS * 3600,
|
|
174
|
+
acceptance_criteria: unique(criteria),
|
|
175
|
+
expected_artifacts: unique(expected),
|
|
176
|
+
};
|
|
177
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type IdentityLink } from './auth/links.js';
|
|
2
|
+
export declare const DEFAULT_PORT = 8097;
|
|
3
|
+
export declare const DEFAULT_MAX_BLOB_BYTES: number;
|
|
4
|
+
export declare const DEFAULT_EVENT_RETENTION_DAYS = 30;
|
|
5
|
+
export interface ServiceClientConfig {
|
|
6
|
+
url: string;
|
|
7
|
+
clientId: string;
|
|
8
|
+
clientSecret: string;
|
|
9
|
+
/** OAuth scope for the client_credentials request. Oathweave grants nothing without one. */
|
|
10
|
+
scope: string;
|
|
11
|
+
}
|
|
12
|
+
export interface Config {
|
|
13
|
+
host: string;
|
|
14
|
+
port: number;
|
|
15
|
+
dataDir: string;
|
|
16
|
+
maxBlobBytes: number;
|
|
17
|
+
eventRetentionDays: number;
|
|
18
|
+
logLevel: 'debug' | 'info' | 'warn' | 'error';
|
|
19
|
+
oathweave: {
|
|
20
|
+
issuer: string;
|
|
21
|
+
jwksUrl: string;
|
|
22
|
+
userinfoUrl: string;
|
|
23
|
+
tokenUrl: string;
|
|
24
|
+
identities: IdentityLink[];
|
|
25
|
+
};
|
|
26
|
+
magnus: ServiceClientConfig;
|
|
27
|
+
hivemem: ServiceClientConfig;
|
|
28
|
+
chattermost: {
|
|
29
|
+
webhookUrl: string;
|
|
30
|
+
} | undefined;
|
|
31
|
+
}
|
|
32
|
+
export declare class ConfigError extends Error {
|
|
33
|
+
readonly issues: ReadonlyArray<{
|
|
34
|
+
name: string;
|
|
35
|
+
message: string;
|
|
36
|
+
}>;
|
|
37
|
+
constructor(issues: ReadonlyArray<{
|
|
38
|
+
name: string;
|
|
39
|
+
message: string;
|
|
40
|
+
}>);
|
|
41
|
+
}
|
|
42
|
+
/** Every variable the service reads. Secrets are marked so tooling never prints their values. */
|
|
43
|
+
export declare const ENV_CATALOG: ReadonlyArray<{
|
|
44
|
+
name: string;
|
|
45
|
+
required: boolean;
|
|
46
|
+
secret: boolean;
|
|
47
|
+
description: string;
|
|
48
|
+
}>;
|
|
49
|
+
type Env = Record<string, string | undefined>;
|
|
50
|
+
/** Read and validate the environment. Throws ConfigError naming every problem; values are never echoed. */
|
|
51
|
+
export declare function loadConfig(env?: Env): Config;
|
|
52
|
+
export {};
|