@obversa/runtime 0.2.0 → 0.2.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.
@@ -0,0 +1,26 @@
1
+ import { type JsonObject, type JsonValue } from '../graph/value.js';
2
+ import type { InteractionBinding, InteractionResponse, Job, JobContext, Outcome } from './types.js';
3
+ export type { InteractionBinding, InteractionResponse } from './types.js';
4
+ /** Keep declared job identity separate from an engine's mutable call state. */
5
+ export declare function interactionDeclaration<T extends Function>(job: T, declaration: unknown): T;
6
+ export declare function interactionIdentity(value: unknown): string;
7
+ export declare function jsonSnapshot(value: unknown): JsonObject;
8
+ /** Save only JSON outcome fields; runtime Error objects are not continuation data. */
9
+ export declare function outcomeSnapshot(outcome: Outcome): JsonObject;
10
+ export declare function savedInteraction(ctx: JobContext, path: readonly string[], identity: string): JsonObject | undefined;
11
+ export declare function hasSavedInteraction(ctx: JobContext, path: readonly string[]): boolean;
12
+ export declare function checkpointInteraction(ctx: JobContext, path: readonly string[], identity: string, data: JsonObject | null): void;
13
+ export declare function interactionResponse(value: unknown): InteractionResponse;
14
+ export declare const DEFAULT_INTERACTION: InteractionBinding;
15
+ export declare function requestInteraction(binding: InteractionBinding, question: string, input: JsonObject, ctx: JobContext, humanApproval?: boolean): Promise<{
16
+ response: InteractionResponse;
17
+ } | {
18
+ paused: Outcome;
19
+ }>;
20
+ export interface HumanReviewOptions {
21
+ readonly question: string;
22
+ readonly input: JsonValue | ((ctx: JobContext) => JsonValue | Promise<JsonValue>);
23
+ readonly interaction: InteractionBinding;
24
+ }
25
+ /** A person's explicit approval is the only passing result of this review. */
26
+ export declare function humanReview(name: string, options: HumanReviewOptions): Job;
@@ -5,6 +5,7 @@
5
5
  * an HTTP API, or any framework — swap the engine and the same job runs.
6
6
  */
7
7
  import type { Outcome, Job, JobContext, ProofArtifact } from './types.js';
8
+ import { type InteractionBinding } from './interaction.js';
8
9
  /** Shared state key holding every engine answer the run recorded so far.
9
10
  * Written by the runtime beside each engine:usage event; read by workflow
10
11
  * layers that must compare what answered against what was declared. */
@@ -26,6 +27,8 @@ import { type LoopErrorCode } from './errors.js';
26
27
  import { type AgentDef } from './agent.js';
27
28
  import { kickback, revisionRequest } from './feedback.js';
28
29
  export interface AgentJobConfig {
30
+ /** Opt in to requesting a rich interaction and continuing this same model. */
31
+ readonly interaction?: InteractionBinding;
29
32
  /** Tag this job's engine answers with the review side and stage they
30
33
  * belong to, so a workflow layer can compare recorded sides without
31
34
  * inferring them from paths. Untagged answers are outside such gates. */
@@ -7,6 +7,8 @@
7
7
  * The default question set and the routing rule below make a loop that was
8
8
  * first hand-wired as dag nodes into a runtime primitive.
9
9
  */
10
+ import { type InteractionBinding, type InteractionResponse } from './interaction.js';
11
+ import type { Outcome } from './types.js';
10
12
  import type { FeedbackActionSeverity, FeedbackFinding, Judge, JudgeAnswer, JudgeQuestions, JobContext } from './types.js';
11
13
  export type { Judge, JudgeAnswer, JudgeQuestion, JudgeQuestions } from './types.js';
12
14
  /**
@@ -25,6 +27,7 @@ export declare function stopQuestions(what?: string): JudgeQuestions;
25
27
  export declare function judge(seat: Judge['seat'], opts: {
26
28
  cap: number;
27
29
  questions?: JudgeQuestions;
30
+ interaction?: InteractionBinding;
28
31
  }): Judge;
29
32
  export declare function isJudge(value: unknown): value is Judge;
30
33
  /** A finding tagged block, by any reviewer, in this round's findings. */
@@ -41,6 +44,7 @@ export interface JudgeRound {
41
44
  }
42
45
  /** What the judge sees. `cap` rides along so it can reason about how much room is left. */
43
46
  export interface JudgeState {
47
+ readonly productFeedback?: readonly InteractionResponse[];
44
48
  readonly useCase?: string;
45
49
  /** The file being refined, relative to the workspace, when there is one. */
46
50
  readonly file?: string;
@@ -56,6 +60,15 @@ export interface JudgeDecision {
56
60
  readonly again: boolean;
57
61
  /** Always names the judge, so a kickback or a stop is traceable to it. */
58
62
  readonly reason: string;
63
+ /**
64
+ * What a stop means, when `again` is false. `ship`: the work holds as it
65
+ * is, so it stands as a pass carrying the judge's reason. `fail`: another
66
+ * round will not fix it, so the run stops there and the requesting side's
67
+ * own failure stands. `product_decision` asks a person, then returns the
68
+ * feedback to this judge without advancing the review round. Absent when
69
+ * `again` is true.
70
+ */
71
+ readonly stop?: 'ship' | 'fail' | 'product_decision';
59
72
  }
60
73
  /**
61
74
  * Route on the judge's answers. The chosen `stop_reason` is read first: in
@@ -66,6 +79,17 @@ export interface JudgeDecision {
66
79
  * here: the caller enforces the cap itself (a loop's own `maxReviewRestarts`,
67
80
  * or a dag's own kickback budget), and a block finding never reaches this
68
81
  * function, it always goes back without asking the judge.
82
+ *
83
+ * `product_decision` pauses for rich feedback and another judgment of the
84
+ * same work. For a terminal stop, `holds` and `over_polishing` say the work
85
+ * is good enough as it stands, so it ships as a pass; other stops, including
86
+ * `not_converging` and any choice a custom question set invents of its own,
87
+ * says the run should not ship silently, so it stops there and the
88
+ * requesting side's failure stands. The same split applies to the
89
+ * probability fallback: a clear `holds` or a clear "not worth doing" ships
90
+ * the work; an unclear "another round is not worth it" fails instead of
91
+ * shipping, since its own criteria already blend polish with a stall and
92
+ * cannot tell the two apart.
69
93
  */
70
94
  export declare function judgeDecision(answers: Readonly<Record<string, JudgeAnswer>>): JudgeDecision;
71
95
  /**
@@ -81,3 +105,15 @@ export declare function askJudge(cfg: Judge, state: JudgeState, ctx: JobContext,
81
105
  answers: Readonly<Record<string, JudgeAnswer>>;
82
106
  decision: JudgeDecision;
83
107
  }>;
108
+ /** Resume only the deliberate product question; every returned answer goes back to this judge. */
109
+ export declare function consultJudge(cfg: Judge, initialState: JudgeState, ctx: JobContext, path: readonly string[], options: {
110
+ readonly identity: string;
111
+ readonly pending: boolean;
112
+ readonly save: (state: JudgeState) => void;
113
+ }): Promise<{
114
+ state: JudgeState;
115
+ decision: JudgeDecision;
116
+ } | {
117
+ state: JudgeState;
118
+ paused: Outcome;
119
+ }>;
@@ -18,9 +18,20 @@ import type { Engine, EngineRef, UsageReceipt } from '../engines/engine.js';
18
18
  import type { Memory, TeamSeat } from '@obversa/api';
19
19
  import type { LoopError } from './errors.js';
20
20
  import type { EnvHandle, Environment } from '../env/environment.js';
21
- import type { JsonValue, RunBrief } from '../graph/value.js';
21
+ import type { JsonObject, JsonValue, RunBrief } from '../graph/value.js';
22
22
  import type { CallbackEvent, ClaimResult, ReleaseResult, SubmitResult } from '../callback/client.js';
23
23
  import type { CallbackRequest } from '../callback/gate.js';
24
+ export type InteractionResponse = JsonObject & {
25
+ readonly feedback: JsonValue;
26
+ readonly prompt: string;
27
+ readonly decision?: 'approved' | 'changes-requested';
28
+ };
29
+ export interface InteractionBinding {
30
+ readonly id: string;
31
+ readonly responseSchema: JsonObject;
32
+ /** Return no answer when the page is cancelled, interrupted or times out. */
33
+ readonly answer?: (request: CallbackRequest, signal: AbortSignal) => Promise<InteractionResponse | undefined>;
34
+ }
24
35
  /**
25
36
  * The client a run's questions go through: the in-memory `CallbackClient`,
26
37
  * or the stored client (`createStoredCallbackClient`) whose questions survive
@@ -79,6 +90,26 @@ export interface Outcome {
79
90
  */
80
91
  revision?: RevisionRequest;
81
92
  }
93
+ export type RecordedStage = {
94
+ readonly kind: 'interrupted';
95
+ readonly startLine: number;
96
+ } | {
97
+ readonly kind: 'completed';
98
+ readonly outcome: Outcome;
99
+ };
100
+ export interface ResumedStageRecords {
101
+ readonly interactions: ReadonlyMap<string, {
102
+ identity: string;
103
+ workspace: string;
104
+ data: JsonObject;
105
+ }>;
106
+ readonly anchors: ReadonlyMap<string, {
107
+ readonly identity: string;
108
+ readonly workspace: string;
109
+ readonly recordId: string;
110
+ }>;
111
+ readonly stages: ReadonlyMap<string, RecordedStage>;
112
+ }
82
113
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
83
114
  /**
84
115
  * Where a job's code lives: a working directory and (when it is a git repo) the
@@ -413,6 +444,7 @@ export interface Judge {
413
444
  readonly seat: TeamSeat;
414
445
  readonly cap: number;
415
446
  readonly questions: JudgeQuestions;
447
+ readonly interaction?: InteractionBinding;
416
448
  }
417
449
  export type KickbackBudget = number | Readonly<Record<string, number | Judge>>;
418
450
  export interface DagConfig {
@@ -685,6 +717,13 @@ export type LoopEvent = {
685
717
  path: string[];
686
718
  answers: Readonly<Record<string, JudgeAnswer>>;
687
719
  reason: string;
720
+ } | {
721
+ kind: 'interaction:checkpoint';
722
+ ts: number;
723
+ path: string[];
724
+ identity: string;
725
+ workspace: string;
726
+ data: import('../graph/value.js').JsonObject | null;
688
727
  } | {
689
728
  kind: 'log';
690
729
  ts: number;
@@ -1,31 +1,18 @@
1
+ import type { UsageReceipt } from '../engines/engine.js';
1
2
  import type { RecordedEngineUsage } from '../core/job.js';
2
- import type { LoopEvent, Outcome } from '../core/types.js';
3
+ import type { LoopEvent, ResumedStageRecords } from '../core/types.js';
4
+ export type { RecordedStage, ResumedStageRecords } from '../core/types.js';
3
5
  interface RecorderOptions {
4
6
  thin?: boolean;
5
7
  /** Append to the existing record instead of truncating it. */
6
8
  resume?: boolean;
7
9
  }
8
- export type RecordedStage = {
9
- readonly kind: 'interrupted';
10
- readonly startLine: number;
11
- } | {
12
- readonly kind: 'completed';
13
- readonly outcome: Outcome;
14
- };
15
- export interface ResumedStageRecords {
16
- readonly anchors: ReadonlyMap<string, {
17
- readonly identity: string;
18
- readonly workspace: string;
19
- readonly recordId: string;
20
- }>;
21
- readonly stages: ReadonlyMap<string, RecordedStage>;
22
- }
23
10
  /** Read the latest stage state and the engine answers recorded for each stage.
24
11
  * A missing record has no stage state, so resume starts fresh. */
25
12
  export declare function readResumeRecord(path: string): {
13
+ readonly receipts: readonly UsageReceipt[];
26
14
  readonly outcomes: ResumedStageRecords;
27
15
  readonly usage: ReadonlyMap<string, readonly RecordedEngineUsage[]>;
28
16
  };
29
17
  /** Append every durable event as one JSON line. */
30
18
  export declare function makeRecorder(path: string, options?: RecorderOptions): (event: LoopEvent) => void;
31
- export {};
@@ -1,4 +1,4 @@
1
- export { INVALID_TEAM_DECISION, assertDistinctSeats, outcomeFromAgentText, panelReviewers, requireNoFiles, requireNonEmptyFiles, seatIdentity, teamAgent } from './chunk-ZXVFWRW5.js';
1
+ export { INVALID_TEAM_DECISION, assertDistinctSeats, outcomeFromAgentText, panelReviewers, requireNoFiles, requireNonEmptyFiles, seatIdentity, teamAgent } from './chunk-G2OIKOUI.js';
2
2
  import './chunk-NIBHM5I5.js';
3
3
  import './chunk-5GLEABOU.js';
4
4
  //# sourceMappingURL=workflow-support.js.map
@@ -1,5 +1,6 @@
1
1
  import { type TeamSeat } from '@obversa/api';
2
2
  import type { Job, Judge, Outcome, ConditionInput } from './core/types.js';
3
+ import { type InteractionBinding } from './core/interaction.js';
3
4
  export interface BriefSource {
4
5
  readonly brief: string;
5
6
  readonly files?: readonly string[];
@@ -7,6 +8,7 @@ export interface BriefSource {
7
8
  export interface PersonRole {
8
9
  readonly kind: 'person';
9
10
  readonly question: string;
11
+ readonly interaction?: InteractionBinding;
10
12
  }
11
13
  export type WorkflowRole = TeamSeat | readonly TeamSeat[] | PersonRole;
12
14
  export interface WorkflowStageBase {
@@ -33,22 +35,32 @@ export type WorkflowStage = WorkflowStageBase & {} & ({
33
35
  readonly run?: never;
34
36
  readonly panel?: never;
35
37
  readonly input?: never;
38
+ readonly fn?: never;
36
39
  } | {
37
40
  readonly run: string | readonly string[];
38
41
  readonly agent?: never;
39
42
  readonly panel?: never;
40
43
  readonly input?: never;
44
+ readonly fn?: never;
41
45
  } | {
42
46
  readonly panel: string;
43
47
  readonly agree?: number;
44
48
  readonly agent?: never;
45
49
  readonly run?: never;
46
50
  readonly input?: never;
51
+ readonly fn?: never;
47
52
  } | {
48
53
  readonly input: string;
49
54
  readonly agent?: never;
50
55
  readonly run?: never;
51
56
  readonly panel?: never;
57
+ readonly fn?: never;
58
+ } | {
59
+ readonly fn: Job;
60
+ readonly agent?: never;
61
+ readonly run?: never;
62
+ readonly panel?: never;
63
+ readonly input?: never;
52
64
  });
53
65
  export interface NamedStage {
54
66
  readonly name: string;
@@ -74,6 +86,8 @@ export interface WorkflowConfig {
74
86
  };
75
87
  }
76
88
  export declare function briefFromFile(path: string | URL): BriefSource;
77
- export declare function person(question: string): PersonRole;
89
+ export declare function person(question: string, options?: {
90
+ interaction?: InteractionBinding;
91
+ }): PersonRole;
78
92
  export declare function stage(name: string, config: WorkflowStage): NamedStage;
79
93
  export declare function workflow(name: string, config: WorkflowConfig): Job;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@obversa/runtime",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Model how your team really works. Durable agent workflows, reviews that send work back, no database needed.",
5
5
  "license": "MIT",
6
6
  "author": "Jonny Neill <jonnyneill@hotmail.com>",
@@ -51,16 +51,16 @@
51
51
  "dependencies": {
52
52
  "p-limit": "^7.3.0",
53
53
  "toposort": "^2.0.2",
54
- "@obversa/core": "0.2.0"
54
+ "@obversa/core": "0.2.1"
55
55
  },
56
56
  "peerDependencies": {
57
- "@obversa/api": "0.2.0"
57
+ "@obversa/api": "0.2.1"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "22.12.0",
61
61
  "@types/toposort": "^2.0.7",
62
62
  "execa": "^9.6.1",
63
- "@obversa/api": "0.2.0"
63
+ "@obversa/api": "0.2.1"
64
64
  },
65
65
  "keywords": [
66
66
  "agent",