@openshain/core 0.3.1 → 0.4.1

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.
@@ -42,6 +42,8 @@ export const TOOL_REJECTION_CODES = [
42
42
  "outside_workspace",
43
43
  "invalid_path",
44
44
  "limit_reached",
45
+ "denied",
46
+ "rejected_by_person",
45
47
  ] as const;
46
48
 
47
49
  export type ToolRejectionCode = (typeof TOOL_REJECTION_CODES)[number];
@@ -72,6 +74,29 @@ export interface EventPayloads {
72
74
  "tool.rejected": { callId: string; name: string; code: ToolRejectionCode; reason: string };
73
75
  "human.input_requested": { callId: string; question: string };
74
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
+ };
75
100
  /** What the person said in a session. Becomes a user message in the projection. */
76
101
  "human.message": { text: string };
77
102
  /** A prompt command expanded for the model: its name, where it came from, and the text handed over. */
@@ -84,6 +109,23 @@ export interface EventPayloads {
84
109
  "work.failed": { reason: string; detail: string };
85
110
  }
86
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
+
87
129
  export type EventType = keyof EventPayloads;
88
130
 
89
131
  interface Envelope {
@@ -181,6 +223,47 @@ export const payloadFileSchemas = {
181
223
  }),
182
224
  "human.input_requested": z.looseObject({ call_id: z.string(), question: z.string() }),
183
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
+ }),
184
267
  "human.message": z.looseObject({ text: z.string() }),
185
268
  "prompt.expanded": z.looseObject({ name: z.string(), source: z.string(), text: z.string() }),
186
269
  "usage.recorded": z.discriminatedUnion("kind", [
@@ -455,6 +538,95 @@ const codecs: { [T in EventType]?: Codec<T> } = {
455
538
  toFile: (p) => ({ call_id: p.callId, answer: p.answer }),
456
539
  fromFile: (p) => ({ callId: p.call_id, answer: p.answer }),
457
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
+ },
458
630
  "usage.recorded": {
459
631
  toFile: (p) =>
460
632
  p.kind === "tool_execution"
@@ -44,6 +44,28 @@ export function pendingQuestions(events: readonly AnyEvent[]): PendingQuestion[]
44
44
  .map((e) => ({ callId: e.payload.callId, question: e.payload.question }));
45
45
  }
46
46
 
47
+ export interface PendingApproval {
48
+ approvalId: string;
49
+ call: { callId: string; name: string; input: unknown };
50
+ ruleId: string;
51
+ kind: "approval" | "review";
52
+ approvers?: string[];
53
+ reviewer?: { role: string; name?: string };
54
+ }
55
+
56
+ /** The approvals of the work that have no decision yet, oldest first. */
57
+ export function pendingApprovals(events: readonly AnyEvent[]): PendingApproval[] {
58
+ const decided = new Set(
59
+ events
60
+ .filter((e): e is Event<"approval.decided"> => e.type === "approval.decided")
61
+ .map((e) => e.payload.approvalId),
62
+ );
63
+ return events
64
+ .filter((e): e is Event<"approval.requested"> => e.type === "approval.requested")
65
+ .filter((e) => !decided.has(e.payload.approvalId))
66
+ .map((e) => ({ ...e.payload }));
67
+ }
68
+
47
69
  export interface HistoryCall {
48
70
  callId: string;
49
71
  name: string;
@@ -59,6 +81,8 @@ export interface WorkHistory {
59
81
  /** Calls that were started but have no result: the work stopped while they ran. */
60
82
  unfinished: HistoryCall[];
61
83
  pending: PendingQuestion[];
84
+ /** Calls held for approval that nobody has decided on yet. */
85
+ approvals: PendingApproval[];
62
86
  toolCalls: number;
63
87
  /** Model calls recorded on the work, for a client that counts them against a limit. */
64
88
  modelCalls: number;
@@ -90,6 +114,7 @@ export function workHistory(events: readonly AnyEvent[]): WorkHistory {
90
114
  calls,
91
115
  unfinished: calls.filter((c) => c.isError === undefined && c.rejected === undefined),
92
116
  pending: pendingQuestions(events),
117
+ approvals: pendingApprovals(events),
93
118
  toolCalls: countToolCalls(events),
94
119
  modelCalls: events.filter((e) => e.type === "model.requested").length,
95
120
  };
@@ -35,12 +35,24 @@ export function buildProjection(input: ProjectionInput): Projection {
35
35
  first?.type === "work.created" ? (first as Event<"work.created">).payload.agentName : undefined;
36
36
  const system = [
37
37
  config.profession.instructions.trim(),
38
- `この会社は ${config.company.name}。`,
39
- `依頼する人は ${config.principal.name}(${config.principal.id})。あなたはこの人の代理として働き、この人と話す。あなた自身は ${config.principal.name} ではなく、この会社で働く社員エージェントで、名乗るならそう名乗る。`,
40
- ...(agentName
41
- ? [`あなたの名前は ${agentName}。名乗るときはこの名前と、社員エージェントであることを言う。`]
42
- : []),
43
- "件数、合計、検索の結果は Tool が返した値をそのまま使い、自分で数えたり合計したりしない。各ターンの最後に Runtime が「残り model 呼び出し N 回、Tool 呼び出し M 回」という 1 行を user message として追加する。これは残量の通知で、返事は要らない。依頼が終わったら、何をしたかを要約して終える。",
38
+ [
39
+ "# 立場",
40
+ `この会社は ${config.company.name}。依頼する人は ${config.principal.name}(${config.principal.id})。あなたはこの人の代理として働き、この人と話す。あなた自身は ${config.principal.name} ではなく、この会社で働く社員エージェント。`,
41
+ ...(agentName
42
+ ? [
43
+ `あなたの名前は ${agentName}。名乗るときはこの名前と、社員エージェントであることを言う。`,
44
+ ]
45
+ : []),
46
+ "",
47
+ "# 数字と事実",
48
+ "件数、合計、検索の結果は Tool が返した値をそのまま使う。自分で数え直したり足し直したりしない。日付と時刻は context を呼んで確かめ、推測しない。",
49
+ "",
50
+ "# 残り回数",
51
+ "各ターンの最後に「残り model 呼び出し N 回、Tool 呼び出し M 回」という 1 行が user message として届く。残量の通知なので、返事は要らない。",
52
+ "",
53
+ "# 終わり方",
54
+ "依頼が終わったら、何をしたかと結果の数字を書いて終える。",
55
+ ].join("\n"),
44
56
  ].join("\n\n");
45
57
 
46
58
  const messages: ModelMessage[] = [];