@sigrun/sigrun 0.8.0 → 0.10.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/CHANGELOG.md +55 -87
- package/dist/config.js +47 -0
- package/dist/curation/host.js +125 -21
- package/dist/curation/runs.js +15 -4
- package/dist/extension.d.ts +268 -3
- package/dist/harness/boot.js +113 -3
- package/dist/harness/complete.js +127 -0
- package/dist/harness/decision-tool.js +93 -0
- package/dist/harness/decisions.js +477 -0
- package/dist/harness/event-bus.js +12 -0
- package/dist/harness/interactions.js +81 -0
- package/dist/harness/interactive-tools.js +97 -0
- package/dist/harness/loader.js +243 -1
- package/dist/harness/packages.js +7 -0
- package/dist/harness/published-hooks.js +208 -0
- package/dist/harness/seam-host.js +136 -1
- package/dist/harness/settings.js +41 -0
- package/dist/history/git.js +6 -2
- package/dist/jobs/fire.js +10 -0
- package/dist/jobs/index.js +3 -0
- package/dist/jobs/observe.js +27 -2
- package/dist/jobs/spec.js +15 -1
- package/dist/server/conversation-ops.js +31 -0
- package/dist/server/core-views/jobs-form.js +335 -0
- package/dist/server/core-views/jobs.js +300 -15
- package/dist/server/core-views/skills-tree.js +3 -12
- package/dist/server/core-views/skills.js +11 -23
- package/dist/server/host-bridge.js +53 -1
- package/dist/server/index.js +2 -1
- package/dist/server/routes/extensions.js +5 -0
- package/dist/server/routes/interactions.js +48 -1
- package/dist/server/routes/meta.js +17 -0
- package/dist/server/routes/providers.js +65 -8
- package/dist/server/routes/submissions.js +3 -0
- package/dist/settings/registry.js +18 -0
- package/node_modules/@sigrun/shared/CHANGELOG.md +12 -0
- package/node_modules/@sigrun/shared/dist/decisions.d.ts +95 -0
- package/node_modules/@sigrun/shared/dist/decisions.js +16 -0
- package/node_modules/@sigrun/shared/dist/interactions.d.ts +14 -0
- package/node_modules/@sigrun/shared/dist/jobs.d.ts +4 -0
- package/node_modules/@sigrun/shared/dist/settings.d.ts +32 -0
- package/node_modules/@sigrun/shared/dist/views.d.ts +58 -0
- package/node_modules/@sigrun/shared/package.json +6 -2
- package/package.json +8 -8
- package/webui/dist/assets/index-By63QKOA.js +55 -0
- package/webui/dist/assets/index-D5KowS7k.css +1 -0
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-B3uMNF59.css +0 -1
- package/webui/dist/assets/index-BXsCyod4.js +0 -55
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jobs-editor Task 3: the pure form logic behind the jobs view's
|
|
3
|
+
* "New job" form — the values shape, the field list, the submit
|
|
4
|
+
* envelope read, and the spec assembly. No I/O here: `jobs.ts` owns
|
|
5
|
+
* the surface (the `{edit: true}` params branch, the `createJob`
|
|
6
|
+
* action) and this module owns the form's content.
|
|
7
|
+
*
|
|
8
|
+
* - `JobFormData.model`/`thinking` carry `"default"` (NOT `""`) — it
|
|
9
|
+
* is the choice field's option id AND the omission sentinel
|
|
10
|
+
* (`createSpecOf` drops the spec keys for it).
|
|
11
|
+
* - The schedule is wholly form-owned: `scheduleType` picks the shape
|
|
12
|
+
* (`{cron, timezone?}` / `{every}` / `{at}`) and `schedule` carries
|
|
13
|
+
* the single text value; `timezone` rides cron only.
|
|
14
|
+
* - New jobs are prompt-only with `conversation: "new"` (the binding
|
|
15
|
+
* ruling) — the kind param exists for the edit shape (Task 4), whose
|
|
16
|
+
* field list differs only in the action column.
|
|
17
|
+
*/
|
|
18
|
+
import { NAME_PATTERN, validateJobSpec } from "../../jobs/spec.js";
|
|
19
|
+
/** The thinking choice's options ("default" labels as Default). */
|
|
20
|
+
export const THINKING_OPTIONS = [
|
|
21
|
+
"default",
|
|
22
|
+
"off",
|
|
23
|
+
"minimal",
|
|
24
|
+
"low",
|
|
25
|
+
"medium",
|
|
26
|
+
"high",
|
|
27
|
+
"xhigh",
|
|
28
|
+
"max",
|
|
29
|
+
];
|
|
30
|
+
/** A new job's seeded values (bound at `/form`). */
|
|
31
|
+
export function emptyForm() {
|
|
32
|
+
return {
|
|
33
|
+
name: "",
|
|
34
|
+
description: "",
|
|
35
|
+
scheduleType: "cron",
|
|
36
|
+
schedule: "",
|
|
37
|
+
timezone: "",
|
|
38
|
+
prompt: "",
|
|
39
|
+
model: "default",
|
|
40
|
+
thinking: "default",
|
|
41
|
+
enabled: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* An existing job's form values (the Task-4 edit seed; Task 3 creates
|
|
46
|
+
* only, but the shape ships here). Name stays editable on the field
|
|
47
|
+
* list; model/thinking absent ⇒ `"default"`.
|
|
48
|
+
*/
|
|
49
|
+
export function formFromJob(job) {
|
|
50
|
+
const schedule = job.schedule;
|
|
51
|
+
const action = job.action;
|
|
52
|
+
return {
|
|
53
|
+
name: job.name,
|
|
54
|
+
description: job.description ?? "",
|
|
55
|
+
scheduleType: "cron" in schedule ? "cron" : "every" in schedule ? "every" : "at",
|
|
56
|
+
schedule: "cron" in schedule
|
|
57
|
+
? schedule.cron
|
|
58
|
+
: "every" in schedule
|
|
59
|
+
? schedule.every
|
|
60
|
+
: schedule.at,
|
|
61
|
+
timezone: "cron" in schedule ? (schedule.timezone ?? "") : "",
|
|
62
|
+
prompt: action.kind === "prompt" ? action.prompt : "",
|
|
63
|
+
model: action.kind === "prompt" && action.model !== undefined
|
|
64
|
+
? action.model
|
|
65
|
+
: "default",
|
|
66
|
+
thinking: action.kind === "prompt" && action.thinking !== undefined
|
|
67
|
+
? action.thinking
|
|
68
|
+
: "default",
|
|
69
|
+
enabled: job.enabled,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Whether `value` is a plain object. */
|
|
73
|
+
function isObject(value) {
|
|
74
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
75
|
+
}
|
|
76
|
+
function stringOf(value) {
|
|
77
|
+
return typeof value === "string" ? value : "";
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The submitted form values: form dispatches wrap the fields as
|
|
81
|
+
* `{values: {...}}` (the webui FormBlock contract — the `fieldOf`
|
|
82
|
+
* convention in skills.ts); a plain object resolves too. An ABSENT
|
|
83
|
+
* `enabled` reads `true` (the toggle defaults on; a disabled/absent
|
|
84
|
+
* field never rides the envelope).
|
|
85
|
+
*/
|
|
86
|
+
export function formValuesOf(payload) {
|
|
87
|
+
const source = isObject(payload) && isObject(payload.values) ? payload.values : payload;
|
|
88
|
+
const record = isObject(source) ? source : {};
|
|
89
|
+
const scheduleType = record.scheduleType;
|
|
90
|
+
return {
|
|
91
|
+
name: stringOf(record.name),
|
|
92
|
+
description: stringOf(record.description),
|
|
93
|
+
scheduleType: scheduleType === "every" || scheduleType === "at" ? scheduleType : "cron",
|
|
94
|
+
schedule: stringOf(record.schedule),
|
|
95
|
+
timezone: stringOf(record.timezone),
|
|
96
|
+
prompt: stringOf(record.prompt),
|
|
97
|
+
model: stringOf(record.model) || "default",
|
|
98
|
+
thinking: stringOf(record.thinking) || "default",
|
|
99
|
+
enabled: typeof record.enabled === "boolean" ? record.enabled : true,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Map a `validateJobSpec` field path onto the form's field (the
|
|
104
|
+
* field-scoped action error). Unknown paths are FORM-LEVEL (no field).
|
|
105
|
+
*/
|
|
106
|
+
export function formFieldOf(path) {
|
|
107
|
+
switch (path) {
|
|
108
|
+
case "name":
|
|
109
|
+
return "name";
|
|
110
|
+
case "description":
|
|
111
|
+
return "description";
|
|
112
|
+
case "schedule":
|
|
113
|
+
case "schedule.cron":
|
|
114
|
+
case "schedule.every":
|
|
115
|
+
case "schedule.at":
|
|
116
|
+
return "schedule";
|
|
117
|
+
case "schedule.timezone":
|
|
118
|
+
return "timezone";
|
|
119
|
+
case "action.prompt":
|
|
120
|
+
return "prompt";
|
|
121
|
+
case "action.model":
|
|
122
|
+
return "model";
|
|
123
|
+
case "action.thinking":
|
|
124
|
+
return "thinking";
|
|
125
|
+
default:
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The schedule assembly shared by create and edit-save (review fix:
|
|
131
|
+
* extracted so the rule can never drift between the two) — wholly
|
|
132
|
+
* form-owned: `scheduleType` picks the shape (`{cron, timezone?}` /
|
|
133
|
+
* `{every}` / `{at}`) and `schedule` carries the single text value;
|
|
134
|
+
* `timezone` rides cron only.
|
|
135
|
+
*/
|
|
136
|
+
function scheduleOf(values) {
|
|
137
|
+
return values.scheduleType === "every"
|
|
138
|
+
? { every: values.schedule.trim() }
|
|
139
|
+
: values.scheduleType === "at"
|
|
140
|
+
? { at: values.schedule.trim() }
|
|
141
|
+
: values.timezone.trim() === ""
|
|
142
|
+
? { cron: values.schedule.trim() }
|
|
143
|
+
: { cron: values.schedule.trim(), timezone: values.timezone.trim() };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Assemble the `JobSpec` for `createApi` from the form values, or
|
|
147
|
+
* reject with the field-scoped failure. New jobs are prompt-only with
|
|
148
|
+
* `conversation: "new"`; the `"default"` model/thinking omit the spec
|
|
149
|
+
* keys (the server default applies).
|
|
150
|
+
*/
|
|
151
|
+
export function createSpecOf(values) {
|
|
152
|
+
const schedule = scheduleOf(values);
|
|
153
|
+
const spec = {
|
|
154
|
+
name: values.name,
|
|
155
|
+
...(values.description.trim() === ""
|
|
156
|
+
? {}
|
|
157
|
+
: { description: values.description.trim() }),
|
|
158
|
+
schedule,
|
|
159
|
+
action: {
|
|
160
|
+
kind: "prompt",
|
|
161
|
+
prompt: values.prompt,
|
|
162
|
+
...(values.model === "default" ? {} : { model: values.model }),
|
|
163
|
+
...(values.thinking === "default" ? {} : { thinking: values.thinking }),
|
|
164
|
+
conversation: "new",
|
|
165
|
+
},
|
|
166
|
+
enabled: values.enabled,
|
|
167
|
+
};
|
|
168
|
+
const validated = validateJobSpec(spec);
|
|
169
|
+
if (!validated.ok) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
field: formFieldOf(validated.field) ?? validated.field,
|
|
173
|
+
message: validated.message,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return { ok: true, spec: validated.spec };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The edit-save patch (jobs-editor Task 4, spec §4 save rules).
|
|
180
|
+
*
|
|
181
|
+
* `updateApi` merges ONE level deep (`{...prior.spec, ...patch}`), so
|
|
182
|
+
* every key the patch carries must be the WHOLE desired value:
|
|
183
|
+
*
|
|
184
|
+
* - the schedule is wholly form-owned (assembled from `scheduleType`
|
|
185
|
+
* + `schedule` + `timezone`; switching type leaves no second
|
|
186
|
+
* discriminant — cron→every drops the timezone);
|
|
187
|
+
* - a PROMPT job's action is `{...prior.action, prompt}` with
|
|
188
|
+
* `model`/`thinking` set from the form or DELETED for `"default"`
|
|
189
|
+
* (never a fresh literal — `conversation`, `runSharing`, `deliver`
|
|
190
|
+
* carry over untouched);
|
|
191
|
+
* - a TASK job's patch carries NO `action` key at all (the editor
|
|
192
|
+
* hides prompt/model/thinking; task + input carry over);
|
|
193
|
+
* - an emptied description becomes an OWN KEY `description: undefined`
|
|
194
|
+
* after validation, so the spread overrides the prior value and the
|
|
195
|
+
* stored record + hash drop it.
|
|
196
|
+
*
|
|
197
|
+
* For field-scoped errors the patch is spread over the prior spec
|
|
198
|
+
* (rebuilt from the flattened wire row — owner is not part of a spec)
|
|
199
|
+
* and the MERGED object is validated with `validateJobSpec`.
|
|
200
|
+
*/
|
|
201
|
+
export function savePatchOf(values, prior) {
|
|
202
|
+
const schedule = scheduleOf(values);
|
|
203
|
+
const patch = {
|
|
204
|
+
name: values.name,
|
|
205
|
+
schedule,
|
|
206
|
+
enabled: values.enabled,
|
|
207
|
+
};
|
|
208
|
+
if (prior.action.kind === "prompt") {
|
|
209
|
+
const action = { ...prior.action, prompt: values.prompt };
|
|
210
|
+
if (values.model === "default") {
|
|
211
|
+
delete action.model;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
action.model = values.model;
|
|
215
|
+
}
|
|
216
|
+
if (values.thinking === "default") {
|
|
217
|
+
delete action.thinking;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
action.thinking = values.thinking;
|
|
221
|
+
}
|
|
222
|
+
patch.action = action;
|
|
223
|
+
}
|
|
224
|
+
// Field-scoped validation on the MERGED spec (the wire is
|
|
225
|
+
// flattened; owner lives on the record, not the spec).
|
|
226
|
+
const priorSpec = {
|
|
227
|
+
name: prior.name,
|
|
228
|
+
...(prior.description === undefined
|
|
229
|
+
? {}
|
|
230
|
+
: { description: prior.description }),
|
|
231
|
+
schedule: prior.schedule,
|
|
232
|
+
action: prior.action,
|
|
233
|
+
...(prior.sharing === undefined ? {} : { sharing: prior.sharing }),
|
|
234
|
+
enabled: prior.enabled,
|
|
235
|
+
};
|
|
236
|
+
const merged = {
|
|
237
|
+
...priorSpec,
|
|
238
|
+
...patch,
|
|
239
|
+
...(values.description.trim() === ""
|
|
240
|
+
? {}
|
|
241
|
+
: { description: values.description.trim() }),
|
|
242
|
+
};
|
|
243
|
+
const validated = validateJobSpec(merged);
|
|
244
|
+
if (!validated.ok) {
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
field: formFieldOf(validated.field),
|
|
248
|
+
message: validated.message,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
// Only AFTER validation does the cleared description land as the
|
|
252
|
+
// own key whose undefined value the spread + JSON round-trip drop.
|
|
253
|
+
if (values.description.trim() === "") {
|
|
254
|
+
patch.description = undefined;
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
patch.description = values.description.trim();
|
|
258
|
+
}
|
|
259
|
+
return { ok: true, patch };
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The form's field list (the same for new and edit; Name stays
|
|
263
|
+
* editable). For `kind: "task"` the prompt/model/thinking fields are
|
|
264
|
+
* omitted (a task job names a task id instead — Task 4's edit shape).
|
|
265
|
+
*/
|
|
266
|
+
export function jobFormFields(kind) {
|
|
267
|
+
const fields = [
|
|
268
|
+
{
|
|
269
|
+
type: "text",
|
|
270
|
+
name: "name",
|
|
271
|
+
label: "Name",
|
|
272
|
+
required: true,
|
|
273
|
+
pattern: NAME_PATTERN.source,
|
|
274
|
+
help: "lowercase letters, digits, dot, dash, underscore",
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
type: "text",
|
|
278
|
+
name: "description",
|
|
279
|
+
label: "Description",
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
type: "choice",
|
|
283
|
+
name: "scheduleType",
|
|
284
|
+
label: "Schedule type",
|
|
285
|
+
options: [
|
|
286
|
+
{ id: "cron", label: "Cron" },
|
|
287
|
+
{ id: "every", label: "Every" },
|
|
288
|
+
{ id: "at", label: "Once" },
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
type: "text",
|
|
293
|
+
name: "schedule",
|
|
294
|
+
label: "Schedule",
|
|
295
|
+
required: true,
|
|
296
|
+
help: "Cron: 0 9 * * 1-5 · Every: 15m or 2h (60s minimum) · Once: an ISO date-time",
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
type: "text",
|
|
300
|
+
name: "timezone",
|
|
301
|
+
label: "Timezone",
|
|
302
|
+
help: "Cron only, e.g. America/Toronto; empty = server timezone",
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
if (kind === "prompt") {
|
|
306
|
+
fields.push({
|
|
307
|
+
type: "longtext",
|
|
308
|
+
name: "prompt",
|
|
309
|
+
label: "Prompt",
|
|
310
|
+
required: true,
|
|
311
|
+
syntax: "markdown",
|
|
312
|
+
rows: 10,
|
|
313
|
+
}, {
|
|
314
|
+
type: "choice",
|
|
315
|
+
name: "model",
|
|
316
|
+
label: "Model",
|
|
317
|
+
options: { $: "/modelOptions" },
|
|
318
|
+
}, {
|
|
319
|
+
type: "choice",
|
|
320
|
+
name: "thinking",
|
|
321
|
+
label: "Thinking",
|
|
322
|
+
options: THINKING_OPTIONS.map((level) => ({
|
|
323
|
+
id: level,
|
|
324
|
+
label: level === "default" ? "Default" : level,
|
|
325
|
+
})),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
fields.push({
|
|
329
|
+
type: "toggle",
|
|
330
|
+
name: "enabled",
|
|
331
|
+
label: "Enabled",
|
|
332
|
+
default: true,
|
|
333
|
+
});
|
|
334
|
+
return fields;
|
|
335
|
+
}
|