@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.
@@ -7,6 +7,9 @@ export const TOOL_REJECTION_CODES = [
7
7
  "reserved_path",
8
8
  "outside_workspace",
9
9
  "invalid_path",
10
+ "limit_reached",
11
+ "denied",
12
+ "rejected_by_person",
10
13
  ];
11
14
  // ---------------------------------------------------------------------------
12
15
  // File-side schemas (snake_case). These are the on-disk contract.
@@ -82,7 +85,47 @@ export const payloadFileSchemas = {
82
85
  }),
83
86
  "human.input_requested": z.looseObject({ call_id: z.string(), question: z.string() }),
84
87
  "human.input_provided": z.looseObject({ call_id: z.string(), answer: z.string() }),
88
+ "approval.requested": z.looseObject({
89
+ approval_id: z.string(),
90
+ call: z.looseObject({ call_id: z.string(), name: z.string(), input: z.unknown() }),
91
+ rule_id: z.string(),
92
+ kind: z.enum(["approval", "review"]),
93
+ approvers: z.array(z.string()).optional(),
94
+ reviewer: z.looseObject({ role: z.string(), name: z.string().optional() }).optional(),
95
+ }),
96
+ "review.requested": z.looseObject({
97
+ approval_id: z.string(),
98
+ package: z.looseObject({
99
+ approval_id: z.string(),
100
+ work_id: z.string(),
101
+ action: z.looseObject({ name: z.string(), tool: z.string(), input: z.unknown() }),
102
+ facts: z.array(z.string()),
103
+ sources: z.array(z.looseObject({
104
+ id: z.string(),
105
+ locator: z.string().optional(),
106
+ version: z.string().optional(),
107
+ })),
108
+ company_rules: z.array(z.looseObject({ id: z.string(), statement: z.string() })),
109
+ proposal: z.string(),
110
+ question: z.string(),
111
+ requested_by: z.string(),
112
+ requested_at: z.iso.datetime(),
113
+ }),
114
+ }),
115
+ "review.decided": z.looseObject({
116
+ approval_id: z.string(),
117
+ decision_id: z.string().optional(),
118
+ }),
119
+ "decision.applied": z.looseObject({ call_id: z.string(), decision_id: z.string() }),
120
+ "approval.decided": z.looseObject({
121
+ approval_id: z.string(),
122
+ decision: z.enum(["approve", "reject", "modify"]),
123
+ by: z.string(),
124
+ comment: z.string().optional(),
125
+ modified_input: z.unknown().optional(),
126
+ }),
85
127
  "human.message": z.looseObject({ text: z.string() }),
128
+ "prompt.expanded": z.looseObject({ name: z.string(), source: z.string(), text: z.string() }),
86
129
  "usage.recorded": z.discriminatedUnion("kind", [
87
130
  z.looseObject({
88
131
  kind: z.literal("model_inference"),
@@ -202,6 +245,20 @@ export function eventFromFile(input) {
202
245
  payload: payloadFromFile(file.type, payload.data),
203
246
  };
204
247
  }
248
+ /**
249
+ * Validates a payload given in the file form (snake_case, as in spec/schemas/events.v1.json) and
250
+ * returns it in the in-memory form. For events a client hands the runtime to record.
251
+ */
252
+ export function parsePayloadFile(type, payload) {
253
+ const parsed = payloadFileSchemas[type].safeParse(payload);
254
+ if (!parsed.success) {
255
+ throw new OpenshainError("invalid_event", `${type} payload: ${describeIssues(parsed.error)}`);
256
+ }
257
+ return payloadFromFile(type, parsed.data);
258
+ }
259
+ export function isKnownEventType(type) {
260
+ return isKnownType(type);
261
+ }
205
262
  function isKnownType(type) {
206
263
  return Object.hasOwn(payloadFileSchemas, type);
207
264
  }
@@ -325,6 +382,95 @@ const codecs = {
325
382
  toFile: (p) => ({ call_id: p.callId, answer: p.answer }),
326
383
  fromFile: (p) => ({ callId: p.call_id, answer: p.answer }),
327
384
  },
385
+ "approval.requested": {
386
+ toFile: (p) => ({
387
+ approval_id: p.approvalId,
388
+ call: { call_id: p.call.callId, name: p.call.name, input: p.call.input },
389
+ rule_id: p.ruleId,
390
+ kind: p.kind,
391
+ ...(p.approvers && { approvers: p.approvers }),
392
+ ...(p.reviewer && { reviewer: p.reviewer }),
393
+ }),
394
+ fromFile: (p) => ({
395
+ approvalId: p.approval_id,
396
+ call: { callId: p.call.call_id, name: p.call.name, input: p.call.input },
397
+ ruleId: p.rule_id,
398
+ kind: p.kind,
399
+ ...(p.approvers && { approvers: p.approvers }),
400
+ ...(p.reviewer && {
401
+ reviewer: {
402
+ role: p.reviewer.role,
403
+ ...(p.reviewer.name !== undefined && { name: p.reviewer.name }),
404
+ },
405
+ }),
406
+ }),
407
+ },
408
+ "review.requested": {
409
+ toFile: (p) => ({
410
+ approval_id: p.approvalId,
411
+ package: {
412
+ approval_id: p.package.approvalId,
413
+ work_id: p.package.workId,
414
+ action: p.package.action,
415
+ facts: p.package.facts,
416
+ sources: p.package.sources,
417
+ company_rules: p.package.companyRules,
418
+ proposal: p.package.proposal,
419
+ question: p.package.question,
420
+ requested_by: p.package.requestedBy,
421
+ requested_at: p.package.requestedAt,
422
+ },
423
+ }),
424
+ fromFile: (p) => ({
425
+ approvalId: p.approval_id,
426
+ package: {
427
+ approvalId: p.package.approval_id,
428
+ workId: p.package.work_id,
429
+ action: p.package.action,
430
+ facts: p.package.facts,
431
+ sources: p.package.sources.map((source) => ({
432
+ id: source.id,
433
+ ...(source.locator !== undefined && { locator: source.locator }),
434
+ ...(source.version !== undefined && { version: source.version }),
435
+ })),
436
+ companyRules: p.package.company_rules,
437
+ proposal: p.package.proposal,
438
+ question: p.package.question,
439
+ requestedBy: p.package.requested_by,
440
+ requestedAt: p.package.requested_at,
441
+ },
442
+ }),
443
+ },
444
+ "review.decided": {
445
+ toFile: (p) => ({
446
+ approval_id: p.approvalId,
447
+ ...(p.decisionId !== undefined && { decision_id: p.decisionId }),
448
+ }),
449
+ fromFile: (p) => ({
450
+ approvalId: p.approval_id,
451
+ ...(p.decision_id !== undefined && { decisionId: p.decision_id }),
452
+ }),
453
+ },
454
+ "decision.applied": {
455
+ toFile: (p) => ({ call_id: p.callId, decision_id: p.decisionId }),
456
+ fromFile: (p) => ({ callId: p.call_id, decisionId: p.decision_id }),
457
+ },
458
+ "approval.decided": {
459
+ toFile: (p) => ({
460
+ approval_id: p.approvalId,
461
+ decision: p.decision,
462
+ by: p.by,
463
+ ...(p.comment !== undefined && { comment: p.comment }),
464
+ ...(p.modifiedInput !== undefined && { modified_input: p.modifiedInput }),
465
+ }),
466
+ fromFile: (p) => ({
467
+ approvalId: p.approval_id,
468
+ decision: p.decision,
469
+ by: p.by,
470
+ ...(p.comment !== undefined && { comment: p.comment }),
471
+ ...(p.modified_input !== undefined && { modifiedInput: p.modified_input }),
472
+ }),
473
+ },
328
474
  "usage.recorded": {
329
475
  toFile: (p) => p.kind === "tool_execution"
330
476
  ? { kind: p.kind, provider: p.provider, usage: { duration_ms: p.usage.durationMs } }
@@ -0,0 +1,57 @@
1
+ import type { AnyEvent } from "./events.ts";
2
+ /** Why a client gives up on a work, as recorded in `work.failed`. */
3
+ export type FailureReason = "limit_reached" | "model_refusal" | "model_error";
4
+ /**
5
+ * Counts the tool calls of a work the way the limits do: every call the runtime started, plus
6
+ * every rejection that never became a call. A rejection of a started call is not a second call.
7
+ */
8
+ export declare function countToolCalls(events: readonly AnyEvent[]): number;
9
+ export interface PendingQuestion {
10
+ callId: string;
11
+ question: string;
12
+ }
13
+ /**
14
+ * The questions of the work that have no answer yet, oldest first. Call ids of questions are
15
+ * minted by the runtime, so the whole log is searched: a client recording its own model turns
16
+ * must not hide a question.
17
+ */
18
+ export declare function pendingQuestions(events: readonly AnyEvent[]): PendingQuestion[];
19
+ export interface PendingApproval {
20
+ approvalId: string;
21
+ call: {
22
+ callId: string;
23
+ name: string;
24
+ input: unknown;
25
+ };
26
+ ruleId: string;
27
+ kind: "approval" | "review";
28
+ approvers?: string[];
29
+ reviewer?: {
30
+ role: string;
31
+ name?: string;
32
+ };
33
+ }
34
+ /** The approvals of the work that have no decision yet, oldest first. */
35
+ export declare function pendingApprovals(events: readonly AnyEvent[]): PendingApproval[];
36
+ export interface HistoryCall {
37
+ callId: string;
38
+ name: string;
39
+ /** The path the call named, when its input had one. */
40
+ path?: string;
41
+ /** Present once the call has a result; absent while it is still open. */
42
+ isError?: boolean;
43
+ rejected?: string;
44
+ }
45
+ export interface WorkHistory {
46
+ calls: HistoryCall[];
47
+ /** Calls that were started but have no result: the work stopped while they ran. */
48
+ unfinished: HistoryCall[];
49
+ pending: PendingQuestion[];
50
+ /** Calls held for approval that nobody has decided on yet. */
51
+ approvals: PendingApproval[];
52
+ toolCalls: number;
53
+ /** Model calls recorded on the work, for a client that counts them against a limit. */
54
+ modelCalls: number;
55
+ }
56
+ /** What a client needs to pick a work up where it stopped. Built from the log alone. */
57
+ export declare function workHistory(events: readonly AnyEvent[]): WorkHistory;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Counts the tool calls of a work the way the limits do: every call the runtime started, plus
3
+ * every rejection that never became a call. A rejection of a started call is not a second call.
4
+ */
5
+ export function countToolCalls(events) {
6
+ let count = 0;
7
+ let started = new Set();
8
+ for (const event of events) {
9
+ if (event.type === "model.completed")
10
+ started = new Set();
11
+ else if (event.type === "tool.called") {
12
+ started.add(event.payload.callId);
13
+ count += 1;
14
+ }
15
+ else if (event.type === "tool.rejected") {
16
+ if (!started.has(event.payload.callId))
17
+ count += 1;
18
+ }
19
+ }
20
+ return count;
21
+ }
22
+ /**
23
+ * The questions of the work that have no answer yet, oldest first. Call ids of questions are
24
+ * minted by the runtime, so the whole log is searched: a client recording its own model turns
25
+ * must not hide a question.
26
+ */
27
+ export function pendingQuestions(events) {
28
+ const answered = new Set(events
29
+ .filter((e) => e.type === "human.input_provided")
30
+ .map((e) => e.payload.callId));
31
+ return events
32
+ .filter((e) => e.type === "human.input_requested")
33
+ .filter((e) => !answered.has(e.payload.callId))
34
+ .map((e) => ({ callId: e.payload.callId, question: e.payload.question }));
35
+ }
36
+ /** The approvals of the work that have no decision yet, oldest first. */
37
+ export function pendingApprovals(events) {
38
+ const decided = new Set(events
39
+ .filter((e) => e.type === "approval.decided")
40
+ .map((e) => e.payload.approvalId));
41
+ return events
42
+ .filter((e) => e.type === "approval.requested")
43
+ .filter((e) => !decided.has(e.payload.approvalId))
44
+ .map((e) => ({ ...e.payload }));
45
+ }
46
+ /** What a client needs to pick a work up where it stopped. Built from the log alone. */
47
+ export function workHistory(events) {
48
+ const calls = [];
49
+ const byId = new Map();
50
+ for (const event of events) {
51
+ if (event.type === "tool.called") {
52
+ const { callId, name, input } = event.payload;
53
+ const path = input?.path;
54
+ const call = { callId, name, ...(typeof path === "string" && { path }) };
55
+ calls.push(call);
56
+ byId.set(callId, call);
57
+ }
58
+ else if (event.type === "tool.completed") {
59
+ const { callId, isError } = event.payload;
60
+ const call = byId.get(callId);
61
+ if (call)
62
+ call.isError = isError;
63
+ }
64
+ else if (event.type === "tool.rejected") {
65
+ const { callId, name, code } = event.payload;
66
+ const call = byId.get(callId);
67
+ if (call)
68
+ call.rejected = code;
69
+ else
70
+ calls.push({ callId, name, rejected: code });
71
+ }
72
+ }
73
+ return {
74
+ calls,
75
+ unfinished: calls.filter((c) => c.isError === undefined && c.rejected === undefined),
76
+ pending: pendingQuestions(events),
77
+ approvals: pendingApprovals(events),
78
+ toolCalls: countToolCalls(events),
79
+ modelCalls: events.filter((e) => e.type === "model.requested").length,
80
+ };
81
+ }
@@ -12,12 +12,24 @@ export function buildProjection(input) {
12
12
  const agentName = first?.type === "work.created" ? first.payload.agentName : undefined;
13
13
  const system = [
14
14
  config.profession.instructions.trim(),
15
- `この会社は ${config.company.name}。`,
16
- `依頼する人は ${config.principal.name}(${config.principal.id})。あなたはこの人の代理として働き、この人と話す。あなた自身は ${config.principal.name} ではなく、この会社で働く社員エージェントで、名乗るならそう名乗る。`,
17
- ...(agentName
18
- ? [`あなたの名前は ${agentName}。名乗るときはこの名前と、社員エージェントであることを言う。`]
19
- : []),
20
- "件数、合計、検索の結果は Tool が返した値をそのまま使い、自分で数えたり合計したりしない。各ターンの最後に Runtime が「残り model 呼び出し N 回、Tool 呼び出し M 回」という 1 行を user message として追加する。これは残量の通知で、返事は要らない。依頼が終わったら、何をしたかを要約して終える。",
15
+ [
16
+ "# 立場",
17
+ `この会社は ${config.company.name}。依頼する人は ${config.principal.name}(${config.principal.id})。あなたはこの人の代理として働き、この人と話す。あなた自身は ${config.principal.name} ではなく、この会社で働く社員エージェント。`,
18
+ ...(agentName
19
+ ? [
20
+ `あなたの名前は ${agentName}。名乗るときはこの名前と、社員エージェントであることを言う。`,
21
+ ]
22
+ : []),
23
+ "",
24
+ "# 数字と事実",
25
+ "件数、合計、検索の結果は Tool が返した値をそのまま使う。自分で数え直したり足し直したりしない。日付と時刻は context を呼んで確かめ、推測しない。",
26
+ "",
27
+ "# 残り回数",
28
+ "各ターンの最後に「残り model 呼び出し N 回、Tool 呼び出し M 回」という 1 行が user message として届く。残量の通知なので、返事は要らない。",
29
+ "",
30
+ "# 終わり方",
31
+ "依頼が終わったら、何をしたかと結果の数字を書いて終える。",
32
+ ].join("\n"),
21
33
  ].join("\n\n");
22
34
  const messages = [];
23
35
  const pushUserPart = (part) => {
@@ -39,6 +51,9 @@ export function buildProjection(input) {
39
51
  case "human.message":
40
52
  pushUserPart({ type: "text", text: event.payload.text });
41
53
  break;
54
+ case "prompt.expanded":
55
+ pushUserPart({ type: "text", text: event.payload.text });
56
+ break;
42
57
  case "model.completed": {
43
58
  const content = event.payload.content
44
59
  .filter((part) => part.type !== "opaque" || part.provider === input.providerId)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openshain/core",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Contracts (provider interfaces), fundamental objects, and the work runtime",
5
5
  "keywords": [
6
6
  "openshain",
@@ -30,7 +30,8 @@
30
30
  "!src/**/*.test.ts",
31
31
  "!src/**/*.test.tsx",
32
32
  "README.md",
33
- "LICENSE"
33
+ "LICENSE",
34
+ "NOTICE"
34
35
  ],
35
36
  "exports": {
36
37
  ".": {