@openshain/core 0.2.0 → 0.4.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/NOTICE +4 -0
- package/dist/authority/policy.d.ts +131 -0
- package/dist/authority/policy.js +335 -0
- package/dist/config/load.js +4 -41
- package/dist/config/schema.d.ts +12 -9
- package/dist/config/schema.js +14 -10
- package/dist/config/yaml.d.ts +12 -0
- package/dist/config/yaml.js +49 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -2
- package/dist/runtime.d.ts +28 -3
- package/dist/runtime.js +148 -6
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.js +3 -0
- package/dist/tool/ask-user.d.ts +5 -0
- package/dist/tool/ask-user.js +22 -0
- package/dist/tool/paths.d.ts +1 -1
- package/dist/tool/paths.js +1 -1
- package/dist/tool/types.js +6 -0
- package/dist/work/events.d.ts +150 -1
- package/dist/work/events.js +146 -0
- package/dist/work/history.d.ts +57 -0
- package/dist/work/history.js +81 -0
- package/dist/work/projection.js +21 -6
- package/package.json +3 -2
- package/src/authority/policy.ts +400 -0
- package/src/config/load.ts +4 -43
- package/src/config/schema.ts +41 -31
- package/src/config/yaml.ts +60 -0
- package/src/index.ts +43 -1
- package/src/runtime.ts +189 -10
- package/src/schemas.ts +17 -1
- package/src/tool/ask-user.ts +25 -0
- package/src/tool/paths.ts +1 -1
- package/src/tool/types.ts +6 -0
- package/src/work/events.ts +192 -0
- package/src/work/history.ts +121 -0
- package/src/work/projection.ts +21 -6
package/src/index.ts
CHANGED
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
// @openshain/core: Contracts (provider interfaces), fundamental objects, and the work runtime
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
AUTHORITY_DIR_NAME,
|
|
5
|
+
type Authority,
|
|
6
|
+
type AuthorityRequest,
|
|
7
|
+
DECISION_KINDS,
|
|
8
|
+
DECISIONS_DIR_NAME,
|
|
9
|
+
DELEGATIONS_FILE_NAME,
|
|
10
|
+
type Decision,
|
|
11
|
+
DecisionFileSchema,
|
|
12
|
+
type DecisionKind,
|
|
13
|
+
type DecisionRecord,
|
|
14
|
+
type Delegation,
|
|
15
|
+
DelegationsFileSchema,
|
|
16
|
+
evaluate,
|
|
17
|
+
loadAuthority,
|
|
18
|
+
matchGlob,
|
|
19
|
+
OPEN_AUTHORITY,
|
|
20
|
+
POLICY_FILE_NAME,
|
|
21
|
+
type PolicyFile,
|
|
22
|
+
PolicyFileSchema,
|
|
23
|
+
type Rule,
|
|
24
|
+
writeDecision,
|
|
25
|
+
} from "./authority/policy.ts";
|
|
2
26
|
export {
|
|
3
27
|
CONFIG_FILE_NAME,
|
|
4
28
|
loadConfig,
|
|
5
29
|
type ParseConfigOptions,
|
|
6
30
|
parseConfig,
|
|
7
31
|
} from "./config/load.ts";
|
|
8
|
-
export type { Config, ToolProviderRef } from "./config/schema.ts";
|
|
32
|
+
export type { Config, ModelConfig, ToolProviderRef } from "./config/schema.ts";
|
|
9
33
|
export { LANGUAGES, type Language } from "./config/schema.ts";
|
|
10
34
|
export { ERROR_CODES, type ErrorCode, isOpenshainError, OpenshainError } from "./errors.ts";
|
|
11
35
|
export {
|
|
@@ -25,16 +49,20 @@ export type {
|
|
|
25
49
|
UserPart,
|
|
26
50
|
} from "./model/types.ts";
|
|
27
51
|
export {
|
|
52
|
+
type CallOptions,
|
|
28
53
|
type CreateRuntimeOptions,
|
|
29
54
|
createRuntime,
|
|
30
55
|
createToolCaller,
|
|
31
56
|
createToolRegistry,
|
|
32
57
|
MAX_TOOL_TEXT_CHARS,
|
|
58
|
+
type PendingApprovalResult,
|
|
59
|
+
REVIEW_DIR_NAME,
|
|
33
60
|
type Runtime,
|
|
34
61
|
type RuntimeProviders,
|
|
35
62
|
type ToolSummary,
|
|
36
63
|
} from "./runtime.ts";
|
|
37
64
|
export { jsonSchemas, type SchemaName } from "./schemas.ts";
|
|
65
|
+
export { ASK_USER, RUNTIME_PROVIDER_ID } from "./tool/ask-user.ts";
|
|
38
66
|
export { loadToolModule } from "./tool/load-module.ts";
|
|
39
67
|
export { RESERVED_PATHS, resolveWorkspacePath } from "./tool/paths.ts";
|
|
40
68
|
export {
|
|
@@ -71,14 +99,28 @@ export {
|
|
|
71
99
|
type EventType,
|
|
72
100
|
eventFromFile,
|
|
73
101
|
eventToFile,
|
|
102
|
+
isKnownEventType,
|
|
74
103
|
type ModelUsage,
|
|
104
|
+
parsePayloadFile,
|
|
75
105
|
payloadFileSchemas,
|
|
106
|
+
type ReviewPackage,
|
|
76
107
|
type StopReason,
|
|
77
108
|
TOOL_REJECTION_CODES,
|
|
78
109
|
type ToolContent,
|
|
79
110
|
type ToolRejectionCode,
|
|
80
111
|
type UnknownEvent,
|
|
81
112
|
} from "./work/events.ts";
|
|
113
|
+
export {
|
|
114
|
+
countToolCalls,
|
|
115
|
+
type FailureReason,
|
|
116
|
+
type HistoryCall,
|
|
117
|
+
type PendingApproval,
|
|
118
|
+
type PendingQuestion,
|
|
119
|
+
pendingApprovals,
|
|
120
|
+
pendingQuestions,
|
|
121
|
+
type WorkHistory,
|
|
122
|
+
workHistory,
|
|
123
|
+
} from "./work/history.ts";
|
|
82
124
|
export { acquireLock, LOCK_FILE_NAME, type Lock } from "./work/lock.ts";
|
|
83
125
|
export { buildProjection, type Projection, type ProjectionInput } from "./work/projection.ts";
|
|
84
126
|
export {
|
package/src/runtime.ts
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
type Authority,
|
|
5
|
+
evaluate,
|
|
6
|
+
loadAuthority,
|
|
7
|
+
OPEN_AUTHORITY,
|
|
8
|
+
type Rule,
|
|
9
|
+
} from "./authority/policy.ts";
|
|
1
10
|
import { loadConfig } from "./config/load.ts";
|
|
2
|
-
import type { Config } from "./config/schema.ts";
|
|
11
|
+
import type { Config, ModelConfig } from "./config/schema.ts";
|
|
3
12
|
import { isOpenshainError, OpenshainError } from "./errors.ts";
|
|
4
13
|
import type { ModelProvider } from "./model/types.ts";
|
|
5
14
|
import { loadToolModule } from "./tool/load-module.ts";
|
|
6
15
|
import type { HiddenTool } from "./tool/registry.ts";
|
|
7
16
|
import { type RegisteredTool, ToolRegistry } from "./tool/registry.ts";
|
|
8
17
|
import type { ToolCall, ToolDefinition, ToolProvider, ToolResult } from "./tool/types.ts";
|
|
9
|
-
import
|
|
18
|
+
import { uuidv7 } from "./uuid.ts";
|
|
19
|
+
import type { Event, ReviewPackage, ToolContent } from "./work/events.ts";
|
|
10
20
|
import { TOOL_REJECTION_CODES, type ToolRejectionCode } from "./work/events.ts";
|
|
11
|
-
import { type WorkHandle, WorkStore } from "./work/store.ts";
|
|
21
|
+
import { WORK_DIR_NAME, type WorkHandle, WorkStore } from "./work/store.ts";
|
|
12
22
|
|
|
13
23
|
export interface RuntimeProviders {
|
|
14
24
|
/** Model providers by the id used in openshain.yaml. */
|
|
15
|
-
models: Record<string, (model:
|
|
25
|
+
models: Record<string, (model: ModelConfig) => ModelProvider>;
|
|
16
26
|
/** Tool providers by the id used in openshain.yaml. Modules are loaded from the config directly. */
|
|
17
27
|
tools: Record<string, () => ToolProvider>;
|
|
18
28
|
}
|
|
@@ -31,6 +41,9 @@ export interface ToolSummary {
|
|
|
31
41
|
/** Longer tool output is cut here so that one tool cannot flood the model's context. */
|
|
32
42
|
export const MAX_TOOL_TEXT_CHARS = 50_000;
|
|
33
43
|
|
|
44
|
+
/** Where a work keeps the review packages a person sends to a reviewer. */
|
|
45
|
+
export const REVIEW_DIR_NAME = "review";
|
|
46
|
+
|
|
34
47
|
export interface Runtime {
|
|
35
48
|
readonly workspaceRoot: string;
|
|
36
49
|
readonly config: Config;
|
|
@@ -50,6 +63,12 @@ export async function createRuntime(options: CreateRuntimeOptions): Promise<Runt
|
|
|
50
63
|
const { workspaceRoot, providers } = options;
|
|
51
64
|
const config = await loadConfig(workspaceRoot, { modelProviders: Object.keys(providers.models) });
|
|
52
65
|
|
|
66
|
+
if (!config.model) {
|
|
67
|
+
throw new OpenshainError(
|
|
68
|
+
"config",
|
|
69
|
+
"this needs a model: add a model section to openshain.yaml (only the interactive CLI needs one)",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
53
72
|
const modelFactory = Object.hasOwn(providers.models, config.model.provider)
|
|
54
73
|
? providers.models[config.model.provider]
|
|
55
74
|
: undefined;
|
|
@@ -66,6 +85,7 @@ export async function createRuntime(options: CreateRuntimeOptions): Promise<Runt
|
|
|
66
85
|
}
|
|
67
86
|
|
|
68
87
|
const registry = await createToolRegistry(workspaceRoot, config, providers.tools);
|
|
88
|
+
const authority = await loadAuthority(workspaceRoot);
|
|
69
89
|
const works = new WorkStore(workspaceRoot);
|
|
70
90
|
return {
|
|
71
91
|
workspaceRoot,
|
|
@@ -75,7 +95,7 @@ export async function createRuntime(options: CreateRuntimeOptions): Promise<Runt
|
|
|
75
95
|
tools: {
|
|
76
96
|
list: () => registry.list().map(({ definition, providerId }) => ({ definition, providerId })),
|
|
77
97
|
hidden: () => registry.hiddenTools(),
|
|
78
|
-
call: createToolCaller({ registry, config, workspaceRoot }),
|
|
98
|
+
call: createToolCaller({ registry, config, workspaceRoot, authority }),
|
|
79
99
|
},
|
|
80
100
|
};
|
|
81
101
|
}
|
|
@@ -85,8 +105,32 @@ export function createToolCaller(input: {
|
|
|
85
105
|
registry: ToolRegistry;
|
|
86
106
|
config: Config;
|
|
87
107
|
workspaceRoot: string;
|
|
88
|
-
|
|
89
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Who may do what. Pass a function when it can change while the server runs, as it does when
|
|
110
|
+
* a reviewer writes a decision. Omitted: the workspace is open, as one without authority/ is.
|
|
111
|
+
*/
|
|
112
|
+
authority?: Authority | (() => Authority);
|
|
113
|
+
}): (work: WorkHandle, call: ToolCall, options?: CallOptions) => Promise<ToolResult> {
|
|
114
|
+
const given = input.authority;
|
|
115
|
+
const current = typeof given === "function" ? given : () => given ?? OPEN_AUTHORITY;
|
|
116
|
+
return (work, call, options) =>
|
|
117
|
+
callTool({ ...input, authority: current(), work, call, ...options });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface CallOptions {
|
|
121
|
+
/** The approval that lets this call run: the policy is not consulted again. */
|
|
122
|
+
approvedBy?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** What a held call answers with: the client shows it to the person and the turn stops there. */
|
|
126
|
+
export interface PendingApprovalResult {
|
|
127
|
+
pending: "approval" | "review";
|
|
128
|
+
approval_id: string;
|
|
129
|
+
rule_id: string;
|
|
130
|
+
approvers?: string[];
|
|
131
|
+
reviewer?: { role: string; name?: string };
|
|
132
|
+
/** For a review: why the policy asks for one, when a cited decision did not cover the call. */
|
|
133
|
+
why?: string;
|
|
90
134
|
}
|
|
91
135
|
|
|
92
136
|
/** Registers the tool providers the config names: the caller's factories by id, and modules from the workspace. Needs no model. */
|
|
@@ -115,8 +159,8 @@ export async function createToolRegistry(
|
|
|
115
159
|
}
|
|
116
160
|
|
|
117
161
|
/**
|
|
118
|
-
* The one place that allows or refuses a call before it runs
|
|
119
|
-
*
|
|
162
|
+
* The one place that allows or refuses a call before it runs by name alone. What the workspace
|
|
163
|
+
* allows a call to do is decided after this, by the policy in `authority/`.
|
|
120
164
|
*/
|
|
121
165
|
function authorize(
|
|
122
166
|
registry: ToolRegistry,
|
|
@@ -137,10 +181,12 @@ async function callTool(input: {
|
|
|
137
181
|
registry: ToolRegistry;
|
|
138
182
|
config: Config;
|
|
139
183
|
workspaceRoot: string;
|
|
184
|
+
authority: Authority;
|
|
140
185
|
work: WorkHandle;
|
|
141
186
|
call: ToolCall;
|
|
187
|
+
approvedBy?: string;
|
|
142
188
|
}): Promise<ToolResult> {
|
|
143
|
-
const { registry, config, workspaceRoot, work, call } = input;
|
|
189
|
+
const { registry, config, workspaceRoot, authority, work, call } = input;
|
|
144
190
|
const reject = async (code: ToolRejectionCode, reason: string): Promise<ToolResult> => {
|
|
145
191
|
await work.append({
|
|
146
192
|
type: "tool.rejected",
|
|
@@ -159,6 +205,72 @@ async function callTool(input: {
|
|
|
159
205
|
`input does not match the schema of ${call.name}: ${validation.reason}`,
|
|
160
206
|
);
|
|
161
207
|
}
|
|
208
|
+
// The policy judges after the allow list, unless a person already approved this very call.
|
|
209
|
+
if (input.approvedBy === undefined) {
|
|
210
|
+
const path = pathOf(call.input);
|
|
211
|
+
const judged = evaluate(authority, {
|
|
212
|
+
tool: call.name,
|
|
213
|
+
effect: tool.definition.effect,
|
|
214
|
+
...(path !== undefined && { path }),
|
|
215
|
+
principal: config.principal.id,
|
|
216
|
+
profession: config.profession.id,
|
|
217
|
+
workType: (await work.current()).type,
|
|
218
|
+
businessDate: businessDate(),
|
|
219
|
+
});
|
|
220
|
+
if (judged.kind === "deny") return reject("denied", judged.reason);
|
|
221
|
+
if (judged.kind === "approval_required" || judged.kind === "review_required") {
|
|
222
|
+
const review = judged.kind === "review_required";
|
|
223
|
+
const approvalId = `apr_${uuidv7()}`;
|
|
224
|
+
const approvers = judged.rule.approvers ?? [config.principal.id];
|
|
225
|
+
const declared = judged.rule.reviewer;
|
|
226
|
+
const reviewer = declared
|
|
227
|
+
? { role: declared.role, ...(declared.name !== undefined && { name: declared.name }) }
|
|
228
|
+
: undefined;
|
|
229
|
+
await work.append({
|
|
230
|
+
type: "approval.requested",
|
|
231
|
+
payload: {
|
|
232
|
+
approvalId,
|
|
233
|
+
call: { callId: call.id, name: call.name, input: call.input },
|
|
234
|
+
ruleId: judged.rule.id,
|
|
235
|
+
kind: review ? "review" : "approval",
|
|
236
|
+
...(review ? reviewer && { reviewer } : { approvers }),
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
if (review) {
|
|
240
|
+
const built = await reviewPackage(work, {
|
|
241
|
+
approvalId,
|
|
242
|
+
call,
|
|
243
|
+
rule: judged.rule,
|
|
244
|
+
principal: config.principal.id,
|
|
245
|
+
});
|
|
246
|
+
await work.append({ type: "review.requested", payload: { approvalId, package: built } });
|
|
247
|
+
// A copy the person can send to the reviewer, next to the work's own record.
|
|
248
|
+
const dir = join(workspaceRoot, WORK_DIR_NAME, work.id, REVIEW_DIR_NAME);
|
|
249
|
+
await mkdir(dir, { recursive: true });
|
|
250
|
+
await writeFile(join(dir, `${approvalId}.json`), `${JSON.stringify(built, null, 2)}\n`, {
|
|
251
|
+
flag: "wx",
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
await work.transition(
|
|
255
|
+
"waiting_approval",
|
|
256
|
+
`rule ${judged.rule.id} needs ${review ? "a review" : "approval"}`,
|
|
257
|
+
);
|
|
258
|
+
const held: PendingApprovalResult = {
|
|
259
|
+
pending: review ? "review" : "approval",
|
|
260
|
+
approval_id: approvalId,
|
|
261
|
+
rule_id: judged.rule.id,
|
|
262
|
+
...(review ? reviewer && { reviewer } : { approvers }),
|
|
263
|
+
...(review && judged.why !== undefined && { why: judged.why }),
|
|
264
|
+
};
|
|
265
|
+
return { content: [{ type: "json", value: held }] };
|
|
266
|
+
}
|
|
267
|
+
if (judged.kind === "allow" && judged.decision) {
|
|
268
|
+
await work.append({
|
|
269
|
+
type: "decision.applied",
|
|
270
|
+
payload: { callId: call.id, decisionId: judged.decision.id },
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
162
274
|
|
|
163
275
|
await work.append({
|
|
164
276
|
type: "tool.called",
|
|
@@ -197,6 +309,73 @@ async function callTool(input: {
|
|
|
197
309
|
return result;
|
|
198
310
|
}
|
|
199
311
|
|
|
312
|
+
/**
|
|
313
|
+
* What the reviewer is asked to decide on, from the work's own record: the call, the tool calls
|
|
314
|
+
* that came before it, and the agent's last words as the proposal. Sources and company rules stay
|
|
315
|
+
* empty until knowledge is in.
|
|
316
|
+
*/
|
|
317
|
+
async function reviewPackage(
|
|
318
|
+
work: WorkHandle,
|
|
319
|
+
input: { approvalId: string; call: ToolCall; rule: Rule; principal: string },
|
|
320
|
+
): Promise<ReviewPackage> {
|
|
321
|
+
const events = await work.events();
|
|
322
|
+
const facts: string[] = [];
|
|
323
|
+
let proposal = "";
|
|
324
|
+
for (const event of events) {
|
|
325
|
+
if (event.type === "tool.called") {
|
|
326
|
+
const { name, input: called } = (event as Event<"tool.called">).payload;
|
|
327
|
+
const path = pathOf(called);
|
|
328
|
+
facts.push(path === undefined ? name : `${name} ${path}`);
|
|
329
|
+
} else if (event.type === "model.completed") {
|
|
330
|
+
const text = (event as Event<"model.completed">).payload.content
|
|
331
|
+
.filter((part) => part.type === "text")
|
|
332
|
+
.map((part) => (part as { text: string }).text)
|
|
333
|
+
.join("\n")
|
|
334
|
+
.trim();
|
|
335
|
+
if (text !== "") proposal = text;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const current = await work.current();
|
|
339
|
+
return {
|
|
340
|
+
approvalId: input.approvalId,
|
|
341
|
+
workId: work.id,
|
|
342
|
+
action: {
|
|
343
|
+
name: input.rule.match.action?.toString() ?? input.call.name,
|
|
344
|
+
tool: input.call.name,
|
|
345
|
+
input: input.call.input,
|
|
346
|
+
},
|
|
347
|
+
facts,
|
|
348
|
+
sources: [],
|
|
349
|
+
companyRules: [],
|
|
350
|
+
proposal,
|
|
351
|
+
question:
|
|
352
|
+
input.rule.reason ??
|
|
353
|
+
`${current.objective} のために ${input.call.name} を実行してよいか、判断をお願いします。`,
|
|
354
|
+
requestedBy: input.principal,
|
|
355
|
+
requestedAt: new Date().toISOString(),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** The path a call names, normalized to a workspace-relative posix path, when its input has one. */
|
|
360
|
+
function pathOf(input: unknown): string | undefined {
|
|
361
|
+
const path = (input as { path?: unknown } | null)?.path;
|
|
362
|
+
if (typeof path !== "string" || path === "") return undefined;
|
|
363
|
+
const segments: string[] = [];
|
|
364
|
+
for (const segment of path.replaceAll("\\", "/").split("/")) {
|
|
365
|
+
if (segment === "" || segment === ".") continue;
|
|
366
|
+
if (segment === "..") segments.pop();
|
|
367
|
+
else segments.push(segment);
|
|
368
|
+
}
|
|
369
|
+
return segments.join("/");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Today's date on this machine's clock, YYYY-MM-DD. */
|
|
373
|
+
function businessDate(): string {
|
|
374
|
+
const now = new Date();
|
|
375
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
376
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
377
|
+
}
|
|
378
|
+
|
|
200
379
|
function isRejectionCode(code: string): code is ToolRejectionCode {
|
|
201
380
|
return (TOOL_REJECTION_CODES as readonly string[]).includes(code);
|
|
202
381
|
}
|
package/src/schemas.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { DelegationsFileSchema, PolicyFileSchema } from "./authority/policy.ts";
|
|
2
3
|
import { ConfigFileSchema } from "./config/schema.ts";
|
|
3
4
|
import type { JsonSchema } from "./tool/types.ts";
|
|
4
5
|
import { EventFileSchema, payloadFileSchemas } from "./work/events.ts";
|
|
5
6
|
import { WorkFileSchema } from "./work/work.ts";
|
|
6
7
|
|
|
7
|
-
export type SchemaName =
|
|
8
|
+
export type SchemaName =
|
|
9
|
+
| "config.v1"
|
|
10
|
+
| "events.v1"
|
|
11
|
+
| "work.v1"
|
|
12
|
+
| "authority-policy.v1"
|
|
13
|
+
| "authority-delegations.v1";
|
|
8
14
|
|
|
9
15
|
/**
|
|
10
16
|
* The JSON Schemas (draft 2020-12) of the files openshain reads and writes, derived from the zod
|
|
@@ -25,6 +31,16 @@ export function jsonSchemas(): Record<SchemaName, JsonSchema> {
|
|
|
25
31
|
"work.json",
|
|
26
32
|
"The state of a work as projected from its event log. Never the source of truth.",
|
|
27
33
|
),
|
|
34
|
+
"authority-policy.v1": describe(
|
|
35
|
+
PolicyFileSchema,
|
|
36
|
+
"authority/policy.yaml",
|
|
37
|
+
"The rules that judge tool calls: the first matching rule decides, else the default.",
|
|
38
|
+
),
|
|
39
|
+
"authority-delegations.v1": describe(
|
|
40
|
+
DelegationsFileSchema,
|
|
41
|
+
"authority/delegations.yaml",
|
|
42
|
+
"Who the agent may act for, as which profession, and when.",
|
|
43
|
+
),
|
|
28
44
|
};
|
|
29
45
|
}
|
|
30
46
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ASK_USER_TOOL_NAME, type ToolDefinition } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
/** The provider id the runtime records for the tools it runs itself. */
|
|
4
|
+
export const RUNTIME_PROVIDER_ID = "runtime";
|
|
5
|
+
|
|
6
|
+
/** The one tool the runtime itself provides: stop and ask the person. */
|
|
7
|
+
export const ASK_USER: Readonly<ToolDefinition> = Object.freeze({
|
|
8
|
+
name: ASK_USER_TOOL_NAME,
|
|
9
|
+
description:
|
|
10
|
+
"Ask the person you work for a question when you cannot proceed without their answer. Use it sparingly; prefer the workspace over guessing. The work waits until the answer is recorded with work_answer.",
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
question: {
|
|
15
|
+
type: "string",
|
|
16
|
+
minLength: 1,
|
|
17
|
+
maxLength: 10_000,
|
|
18
|
+
description: "The question, in the person's language.",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
required: ["question"],
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
},
|
|
24
|
+
effect: "observe",
|
|
25
|
+
});
|
package/src/tool/paths.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { dirname, isAbsolute, join, normalize, relative, resolve, sep } from "no
|
|
|
3
3
|
import { OpenshainError } from "../errors.ts";
|
|
4
4
|
|
|
5
5
|
/** Paths the runtime keeps for itself. Tools may not read or write them. */
|
|
6
|
-
export const RESERVED_PATHS = ["openshain.yaml", "work"] as const;
|
|
6
|
+
export const RESERVED_PATHS = ["openshain.yaml", "work", "principals", "authority"] as const;
|
|
7
7
|
|
|
8
8
|
const MAX_SYMLINK_HOPS = 32;
|
|
9
9
|
|
package/src/tool/types.ts
CHANGED
|
@@ -20,6 +20,12 @@ export const RESERVED_TOOL_NAMES: readonly string[] = [
|
|
|
20
20
|
"work_list",
|
|
21
21
|
"work_complete",
|
|
22
22
|
"work_fail",
|
|
23
|
+
"work_answer",
|
|
24
|
+
"work_record",
|
|
25
|
+
"context",
|
|
26
|
+
"approval_list",
|
|
27
|
+
"approval_decide",
|
|
28
|
+
"review_decide",
|
|
23
29
|
"work_run",
|
|
24
30
|
"work_show",
|
|
25
31
|
];
|
package/src/work/events.ts
CHANGED
|
@@ -41,6 +41,9 @@ export const TOOL_REJECTION_CODES = [
|
|
|
41
41
|
"reserved_path",
|
|
42
42
|
"outside_workspace",
|
|
43
43
|
"invalid_path",
|
|
44
|
+
"limit_reached",
|
|
45
|
+
"denied",
|
|
46
|
+
"rejected_by_person",
|
|
44
47
|
] as const;
|
|
45
48
|
|
|
46
49
|
export type ToolRejectionCode = (typeof TOOL_REJECTION_CODES)[number];
|
|
@@ -71,8 +74,33 @@ export interface EventPayloads {
|
|
|
71
74
|
"tool.rejected": { callId: string; name: string; code: ToolRejectionCode; reason: string };
|
|
72
75
|
"human.input_requested": { callId: string; question: string };
|
|
73
76
|
"human.input_provided": { callId: string; answer: string };
|
|
77
|
+
/** A tool call the policy holds for a person's or a reviewer's approval. The work waits. */
|
|
78
|
+
"approval.requested": {
|
|
79
|
+
approvalId: string;
|
|
80
|
+
call: { callId: string; name: string; input: unknown };
|
|
81
|
+
ruleId: string;
|
|
82
|
+
kind: "approval" | "review";
|
|
83
|
+
approvers?: string[];
|
|
84
|
+
reviewer?: { role: string; name?: string };
|
|
85
|
+
};
|
|
86
|
+
/** The package handed to the reviewer: the call, what the work established, and the question. */
|
|
87
|
+
"review.requested": { approvalId: string; package: ReviewPackage };
|
|
88
|
+
/** The reviewer's answer. A decision id when they wrote one; absent when they refused. */
|
|
89
|
+
"review.decided": { approvalId: string; decisionId?: string };
|
|
90
|
+
/** A call that ran because an approved decision covers it. */
|
|
91
|
+
"decision.applied": { callId: string; decisionId: string };
|
|
92
|
+
/** The answer to an approval: approve runs the call, reject refuses it, modify runs it with the reviewer's input. */
|
|
93
|
+
"approval.decided": {
|
|
94
|
+
approvalId: string;
|
|
95
|
+
decision: "approve" | "reject" | "modify";
|
|
96
|
+
by: string;
|
|
97
|
+
comment?: string;
|
|
98
|
+
modifiedInput?: unknown;
|
|
99
|
+
};
|
|
74
100
|
/** What the person said in a session. Becomes a user message in the projection. */
|
|
75
101
|
"human.message": { text: string };
|
|
102
|
+
/** A prompt command expanded for the model: its name, where it came from, and the text handed over. */
|
|
103
|
+
"prompt.expanded": { name: string; source: string; text: string };
|
|
76
104
|
"usage.recorded":
|
|
77
105
|
| { kind: "model_inference"; provider: string; model: string; usage: ModelUsage }
|
|
78
106
|
| { kind: "tool_execution"; provider: string; usage: { durationMs: number } };
|
|
@@ -81,6 +109,23 @@ export interface EventPayloads {
|
|
|
81
109
|
"work.failed": { reason: string; detail: string };
|
|
82
110
|
}
|
|
83
111
|
|
|
112
|
+
/** What a reviewer is asked to decide on, built from the work's own record. */
|
|
113
|
+
export interface ReviewPackage {
|
|
114
|
+
approvalId: string;
|
|
115
|
+
workId: string;
|
|
116
|
+
action: { name: string; tool: string; input: unknown };
|
|
117
|
+
/** What the work established before this call: the tool calls it made. */
|
|
118
|
+
facts: string[];
|
|
119
|
+
/** Sources and company rules the work cited. Empty until knowledge is in. */
|
|
120
|
+
sources: { id: string; locator?: string; version?: string }[];
|
|
121
|
+
companyRules: { id: string; statement: string }[];
|
|
122
|
+
/** What the agent proposes, in its own words. */
|
|
123
|
+
proposal: string;
|
|
124
|
+
question: string;
|
|
125
|
+
requestedBy: string;
|
|
126
|
+
requestedAt: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
84
129
|
export type EventType = keyof EventPayloads;
|
|
85
130
|
|
|
86
131
|
interface Envelope {
|
|
@@ -178,7 +223,49 @@ export const payloadFileSchemas = {
|
|
|
178
223
|
}),
|
|
179
224
|
"human.input_requested": z.looseObject({ call_id: z.string(), question: z.string() }),
|
|
180
225
|
"human.input_provided": z.looseObject({ call_id: z.string(), answer: z.string() }),
|
|
226
|
+
"approval.requested": z.looseObject({
|
|
227
|
+
approval_id: z.string(),
|
|
228
|
+
call: z.looseObject({ call_id: z.string(), name: z.string(), input: z.unknown() }),
|
|
229
|
+
rule_id: z.string(),
|
|
230
|
+
kind: z.enum(["approval", "review"]),
|
|
231
|
+
approvers: z.array(z.string()).optional(),
|
|
232
|
+
reviewer: z.looseObject({ role: z.string(), name: z.string().optional() }).optional(),
|
|
233
|
+
}),
|
|
234
|
+
"review.requested": z.looseObject({
|
|
235
|
+
approval_id: z.string(),
|
|
236
|
+
package: z.looseObject({
|
|
237
|
+
approval_id: z.string(),
|
|
238
|
+
work_id: z.string(),
|
|
239
|
+
action: z.looseObject({ name: z.string(), tool: z.string(), input: z.unknown() }),
|
|
240
|
+
facts: z.array(z.string()),
|
|
241
|
+
sources: z.array(
|
|
242
|
+
z.looseObject({
|
|
243
|
+
id: z.string(),
|
|
244
|
+
locator: z.string().optional(),
|
|
245
|
+
version: z.string().optional(),
|
|
246
|
+
}),
|
|
247
|
+
),
|
|
248
|
+
company_rules: z.array(z.looseObject({ id: z.string(), statement: z.string() })),
|
|
249
|
+
proposal: z.string(),
|
|
250
|
+
question: z.string(),
|
|
251
|
+
requested_by: z.string(),
|
|
252
|
+
requested_at: z.iso.datetime(),
|
|
253
|
+
}),
|
|
254
|
+
}),
|
|
255
|
+
"review.decided": z.looseObject({
|
|
256
|
+
approval_id: z.string(),
|
|
257
|
+
decision_id: z.string().optional(),
|
|
258
|
+
}),
|
|
259
|
+
"decision.applied": z.looseObject({ call_id: z.string(), decision_id: z.string() }),
|
|
260
|
+
"approval.decided": z.looseObject({
|
|
261
|
+
approval_id: z.string(),
|
|
262
|
+
decision: z.enum(["approve", "reject", "modify"]),
|
|
263
|
+
by: z.string(),
|
|
264
|
+
comment: z.string().optional(),
|
|
265
|
+
modified_input: z.unknown().optional(),
|
|
266
|
+
}),
|
|
181
267
|
"human.message": z.looseObject({ text: z.string() }),
|
|
268
|
+
"prompt.expanded": z.looseObject({ name: z.string(), source: z.string(), text: z.string() }),
|
|
182
269
|
"usage.recorded": z.discriminatedUnion("kind", [
|
|
183
270
|
z.looseObject({
|
|
184
271
|
kind: z.literal("model_inference"),
|
|
@@ -309,6 +396,22 @@ export function eventFromFile(input: unknown): AnyEvent {
|
|
|
309
396
|
} as Event;
|
|
310
397
|
}
|
|
311
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Validates a payload given in the file form (snake_case, as in spec/schemas/events.v1.json) and
|
|
401
|
+
* returns it in the in-memory form. For events a client hands the runtime to record.
|
|
402
|
+
*/
|
|
403
|
+
export function parsePayloadFile<T extends EventType>(type: T, payload: unknown): EventPayloads[T] {
|
|
404
|
+
const parsed = payloadFileSchemas[type].safeParse(payload);
|
|
405
|
+
if (!parsed.success) {
|
|
406
|
+
throw new OpenshainError("invalid_event", `${type} payload: ${describeIssues(parsed.error)}`);
|
|
407
|
+
}
|
|
408
|
+
return payloadFromFile(type, parsed.data as FilePayload<T>);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function isKnownEventType(type: string): type is EventType {
|
|
412
|
+
return isKnownType(type);
|
|
413
|
+
}
|
|
414
|
+
|
|
312
415
|
function isKnownType(type: string): type is EventType {
|
|
313
416
|
return Object.hasOwn(payloadFileSchemas, type);
|
|
314
417
|
}
|
|
@@ -435,6 +538,95 @@ const codecs: { [T in EventType]?: Codec<T> } = {
|
|
|
435
538
|
toFile: (p) => ({ call_id: p.callId, answer: p.answer }),
|
|
436
539
|
fromFile: (p) => ({ callId: p.call_id, answer: p.answer }),
|
|
437
540
|
},
|
|
541
|
+
"approval.requested": {
|
|
542
|
+
toFile: (p) => ({
|
|
543
|
+
approval_id: p.approvalId,
|
|
544
|
+
call: { call_id: p.call.callId, name: p.call.name, input: p.call.input },
|
|
545
|
+
rule_id: p.ruleId,
|
|
546
|
+
kind: p.kind,
|
|
547
|
+
...(p.approvers && { approvers: p.approvers }),
|
|
548
|
+
...(p.reviewer && { reviewer: p.reviewer }),
|
|
549
|
+
}),
|
|
550
|
+
fromFile: (p) => ({
|
|
551
|
+
approvalId: p.approval_id,
|
|
552
|
+
call: { callId: p.call.call_id, name: p.call.name, input: p.call.input },
|
|
553
|
+
ruleId: p.rule_id,
|
|
554
|
+
kind: p.kind,
|
|
555
|
+
...(p.approvers && { approvers: p.approvers }),
|
|
556
|
+
...(p.reviewer && {
|
|
557
|
+
reviewer: {
|
|
558
|
+
role: p.reviewer.role,
|
|
559
|
+
...(p.reviewer.name !== undefined && { name: p.reviewer.name }),
|
|
560
|
+
},
|
|
561
|
+
}),
|
|
562
|
+
}),
|
|
563
|
+
},
|
|
564
|
+
"review.requested": {
|
|
565
|
+
toFile: (p) => ({
|
|
566
|
+
approval_id: p.approvalId,
|
|
567
|
+
package: {
|
|
568
|
+
approval_id: p.package.approvalId,
|
|
569
|
+
work_id: p.package.workId,
|
|
570
|
+
action: p.package.action,
|
|
571
|
+
facts: p.package.facts,
|
|
572
|
+
sources: p.package.sources,
|
|
573
|
+
company_rules: p.package.companyRules,
|
|
574
|
+
proposal: p.package.proposal,
|
|
575
|
+
question: p.package.question,
|
|
576
|
+
requested_by: p.package.requestedBy,
|
|
577
|
+
requested_at: p.package.requestedAt,
|
|
578
|
+
},
|
|
579
|
+
}),
|
|
580
|
+
fromFile: (p) => ({
|
|
581
|
+
approvalId: p.approval_id,
|
|
582
|
+
package: {
|
|
583
|
+
approvalId: p.package.approval_id,
|
|
584
|
+
workId: p.package.work_id,
|
|
585
|
+
action: p.package.action,
|
|
586
|
+
facts: p.package.facts,
|
|
587
|
+
sources: p.package.sources.map((source) => ({
|
|
588
|
+
id: source.id,
|
|
589
|
+
...(source.locator !== undefined && { locator: source.locator }),
|
|
590
|
+
...(source.version !== undefined && { version: source.version }),
|
|
591
|
+
})),
|
|
592
|
+
companyRules: p.package.company_rules,
|
|
593
|
+
proposal: p.package.proposal,
|
|
594
|
+
question: p.package.question,
|
|
595
|
+
requestedBy: p.package.requested_by,
|
|
596
|
+
requestedAt: p.package.requested_at,
|
|
597
|
+
},
|
|
598
|
+
}),
|
|
599
|
+
},
|
|
600
|
+
"review.decided": {
|
|
601
|
+
toFile: (p) => ({
|
|
602
|
+
approval_id: p.approvalId,
|
|
603
|
+
...(p.decisionId !== undefined && { decision_id: p.decisionId }),
|
|
604
|
+
}),
|
|
605
|
+
fromFile: (p) => ({
|
|
606
|
+
approvalId: p.approval_id,
|
|
607
|
+
...(p.decision_id !== undefined && { decisionId: p.decision_id }),
|
|
608
|
+
}),
|
|
609
|
+
},
|
|
610
|
+
"decision.applied": {
|
|
611
|
+
toFile: (p) => ({ call_id: p.callId, decision_id: p.decisionId }),
|
|
612
|
+
fromFile: (p) => ({ callId: p.call_id, decisionId: p.decision_id }),
|
|
613
|
+
},
|
|
614
|
+
"approval.decided": {
|
|
615
|
+
toFile: (p) => ({
|
|
616
|
+
approval_id: p.approvalId,
|
|
617
|
+
decision: p.decision,
|
|
618
|
+
by: p.by,
|
|
619
|
+
...(p.comment !== undefined && { comment: p.comment }),
|
|
620
|
+
...(p.modifiedInput !== undefined && { modified_input: p.modifiedInput }),
|
|
621
|
+
}),
|
|
622
|
+
fromFile: (p) => ({
|
|
623
|
+
approvalId: p.approval_id,
|
|
624
|
+
decision: p.decision,
|
|
625
|
+
by: p.by,
|
|
626
|
+
...(p.comment !== undefined && { comment: p.comment }),
|
|
627
|
+
...(p.modified_input !== undefined && { modifiedInput: p.modified_input }),
|
|
628
|
+
}),
|
|
629
|
+
},
|
|
438
630
|
"usage.recorded": {
|
|
439
631
|
toFile: (p) =>
|
|
440
632
|
p.kind === "tool_execution"
|