@obversa/runtime 0.1.0 → 0.2.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.
@@ -0,0 +1,83 @@
1
+ /**
2
+ * A judge: a seat that answers typed questions about a piece of work and the
3
+ * rounds spent on it so far, in place of a plain review-round count. Used on
4
+ * a `workflow()` stage's `refine` and on a `dag()`'s `maxKickbacks`, between
5
+ * a review's verdict and the send-back.
6
+ *
7
+ * The default question set and the routing rule below make a loop that was
8
+ * first hand-wired as dag nodes into a runtime primitive.
9
+ */
10
+ import type { FeedbackActionSeverity, FeedbackFinding, Judge, JudgeAnswer, JudgeQuestions, JobContext } from './types.js';
11
+ export type { Judge, JudgeAnswer, JudgeQuestion, JudgeQuestions } from './types.js';
12
+ /**
13
+ * The default question set: does the work hold for this use case, are the
14
+ * latest findings worth doing, is another round worth it, and why stop. A
15
+ * caller's own set replaces this wholesale (e.g. for a use case this
16
+ * wording doesn't fit); `what` names the thing being judged in the wording.
17
+ */
18
+ export declare function stopQuestions(what?: string): JudgeQuestions;
19
+ /**
20
+ * A judge for `refine` or `maxKickbacks`: `seat` runs with workspace mode
21
+ * none (it reads no file itself; everything it needs rides in the prompt),
22
+ * `cap` is the hard backstop on rounds regardless of what the judge says,
23
+ * and `questions` defaults to `stopQuestions()`.
24
+ */
25
+ export declare function judge(seat: Judge['seat'], opts: {
26
+ cap: number;
27
+ questions?: JudgeQuestions;
28
+ }): Judge;
29
+ export declare function isJudge(value: unknown): value is Judge;
30
+ /** A finding tagged block, by any reviewer, in this round's findings. */
31
+ export declare function hasBlockFinding(findings: readonly FeedbackFinding[] | undefined): boolean;
32
+ /** Every finding, counted by severity, the judge reads numbers, not a list to recount itself. */
33
+ export declare function countBySeverity(findings: readonly FeedbackFinding[]): Record<FeedbackActionSeverity, number>;
34
+ /** One round of the work the judge is watching converge. */
35
+ export interface JudgeRound {
36
+ readonly round: number;
37
+ readonly findings: readonly FeedbackFinding[];
38
+ readonly counts: Readonly<Record<FeedbackActionSeverity, number>>;
39
+ /** Lines added or removed since the previous round, when the target is a file. */
40
+ readonly changedLines?: number;
41
+ }
42
+ /** What the judge sees. `cap` rides along so it can reason about how much room is left. */
43
+ export interface JudgeState {
44
+ readonly useCase?: string;
45
+ /** The file being refined, relative to the workspace, when there is one. */
46
+ readonly file?: string;
47
+ /** That file's current content, when there is one. */
48
+ readonly draft?: string;
49
+ readonly latestFindings: readonly FeedbackFinding[];
50
+ readonly rounds: readonly JudgeRound[];
51
+ readonly round: number;
52
+ readonly cap: number;
53
+ }
54
+ export interface JudgeDecision {
55
+ /** Send the work back for another round. */
56
+ readonly again: boolean;
57
+ /** Always names the judge, so a kickback or a stop is traceable to it. */
58
+ readonly reason: string;
59
+ }
60
+ /**
61
+ * Route on the judge's answers. The chosen `stop_reason` is read first: in
62
+ * use it moves with the rounds while the probability answers stay flat, so it
63
+ * is the answer that discriminates. A clear yes or no from `holds`,
64
+ * `worth_doing` or `worth_another_round` is the fallback when the choice does
65
+ * not parse. Neither the cap nor a block finding is checked
66
+ * here: the caller enforces the cap itself (a loop's own `maxReviewRestarts`,
67
+ * or a dag's own kickback budget), and a block finding never reaches this
68
+ * function, it always goes back without asking the judge.
69
+ */
70
+ export declare function judgeDecision(answers: Readonly<Record<string, JudgeAnswer>>): JudgeDecision;
71
+ /**
72
+ * Ask the judge, and record its answer. Runs `cfg.seat` as a workspace-mode-
73
+ * none agent turn over `state` and `cfg.questions`, parses the reply as the
74
+ * judge engine's own `{ [question]: JudgeAnswer }` shape, and emits
75
+ * `refine:judge` so a person reading the record sees what it answered and
76
+ * why. A reply that fails to parse becomes an empty answers object ,
77
+ * `judgeDecision` reads that as `stop_reason: 'unknown'`, which is not a
78
+ * chosen stop, so the caller's own cap is what ends the rounds.
79
+ */
80
+ export declare function askJudge(cfg: Judge, state: JudgeState, ctx: JobContext, path: readonly string[]): Promise<{
81
+ answers: Readonly<Record<string, JudgeAnswer>>;
82
+ decision: JudgeDecision;
83
+ }>;
@@ -15,7 +15,7 @@
15
15
  * the primitive is the loop, not a pipeline.
16
16
  */
17
17
  import type { Engine, EngineRef, UsageReceipt } from '../engines/engine.js';
18
- import type { Memory } from '@obversa/api';
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
21
  import type { JsonValue, RunBrief } from '../graph/value.js';
@@ -371,7 +371,50 @@ export interface DagNode {
371
371
  */
372
372
  acceptsKickbackTo?: string[];
373
373
  }
374
- export type KickbackBudget = number | Readonly<Record<string, number>>;
374
+ /**
375
+ * One question put to a judge, in the judge engine's own shape: `noul` asks
376
+ * for a probability, `choice` asks for one of the named criteria, `score`
377
+ * asks for a 0..1 rating per named criterion. `instructions` is what the
378
+ * judge reads; `criteria` are the standards it answers against.
379
+ */
380
+ export type JudgeQuestion = {
381
+ readonly type: 'noul';
382
+ readonly instructions: string;
383
+ readonly criteria: {
384
+ readonly true: string;
385
+ readonly false: string;
386
+ };
387
+ } | {
388
+ readonly type: 'choice';
389
+ readonly instructions: string;
390
+ readonly criteria: Readonly<Record<string, string>>;
391
+ } | {
392
+ readonly type: 'score';
393
+ readonly instructions: string;
394
+ readonly criteria: readonly string[];
395
+ };
396
+ export type JudgeQuestions = Readonly<Record<string, JudgeQuestion>>;
397
+ /** One answer, in whichever of these fields its question type fills. */
398
+ export interface JudgeAnswer {
399
+ readonly noul?: number;
400
+ readonly probability?: number;
401
+ readonly choice?: string;
402
+ readonly probabilities?: Readonly<Record<string, number>>;
403
+ }
404
+ /**
405
+ * A judge, in place of a plain round count, on a `workflow()` stage's
406
+ * `refine` or a `dag()`'s `maxKickbacks`: a seat that answers typed
407
+ * questions about the work and the rounds so far, between a review's
408
+ * verdict and the send-back. `cap` is the hard backstop, reached or not,
409
+ * it always stops the rounds. Built with `judge()`, never by hand.
410
+ */
411
+ export interface Judge {
412
+ readonly kind: 'judge';
413
+ readonly seat: TeamSeat;
414
+ readonly cap: number;
415
+ readonly questions: JudgeQuestions;
416
+ }
417
+ export type KickbackBudget = number | Readonly<Record<string, number | Judge>>;
375
418
  export interface DagConfig {
376
419
  name: string;
377
420
  /** Node name → a `DagNode`, or a bare `Job` (shorthand for no deps/gates). */
@@ -626,6 +669,8 @@ export type LoopEvent = {
626
669
  path: string[];
627
670
  name: string;
628
671
  phase: 'use' | 'result';
672
+ /** The file, command, URL or pattern the tool acted on, when known. */
673
+ target?: string;
629
674
  } | {
630
675
  kind: 'engine:usage';
631
676
  ts: number;
@@ -634,6 +679,12 @@ export type LoopEvent = {
634
679
  usage: UsageReceipt;
635
680
  role?: 'writer' | 'reviewer';
636
681
  stage?: string;
682
+ } | {
683
+ kind: 'refine:judge';
684
+ ts: number;
685
+ path: string[];
686
+ answers: Readonly<Record<string, JudgeAnswer>>;
687
+ reason: string;
637
688
  } | {
638
689
  kind: 'log';
639
690
  ts: number;
@@ -58,13 +58,16 @@ export interface MonitorState {
58
58
  decisionText: string;
59
59
  input: JsonValue;
60
60
  }>;
61
+ /**
62
+ * The record, already in the one line `formatEvent` would print for it: the
63
+ * console and this page must never disagree about what an event says, so
64
+ * neither keeps its own partial fields to re-render from. An event with
65
+ * nothing worth telling a person (thinking with no text yet) contributes no
66
+ * line at all, rather than a row that says only its own kind.
67
+ */
61
68
  events: Array<{
62
- kind: string;
63
69
  ts: number;
64
- path: string[];
65
- node?: string;
66
- label?: string;
67
- summary?: string;
70
+ line: string;
68
71
  }>;
69
72
  }
70
73
  export interface StartedMonitor {
@@ -0,0 +1,68 @@
1
+ import type { LoopEvent } from '../core/types.js';
2
+ export interface RecordLine {
3
+ readonly kind: string;
4
+ readonly text: string;
5
+ }
6
+ export interface RecordEngineCall {
7
+ readonly model: string;
8
+ readonly inputTokens: number | null;
9
+ readonly outputTokens: number | null;
10
+ }
11
+ export interface RecordToolUse {
12
+ readonly name: string;
13
+ readonly count: number;
14
+ readonly targets: readonly string[];
15
+ }
16
+ export interface RecordKickback {
17
+ readonly from: string;
18
+ readonly to: string;
19
+ readonly reason: string;
20
+ readonly count: number;
21
+ readonly limit: number;
22
+ readonly accepted: boolean;
23
+ readonly note?: string;
24
+ }
25
+ export interface RecordOutcome {
26
+ readonly status: string;
27
+ readonly summary?: string;
28
+ }
29
+ export interface RecordNodeRun {
30
+ readonly attempt: number;
31
+ readonly startedAt: number | null;
32
+ readonly endedAt: number | null;
33
+ readonly outcome: RecordOutcome | null;
34
+ readonly kickback: RecordKickback | null;
35
+ readonly engineCalls: readonly RecordEngineCall[];
36
+ readonly tools: readonly RecordToolUse[];
37
+ readonly lines: readonly RecordLine[];
38
+ }
39
+ export interface RecordNodeSummary {
40
+ readonly node: string;
41
+ readonly runs: readonly RecordNodeRun[];
42
+ }
43
+ export interface RecordUsage {
44
+ readonly inputTokens: number;
45
+ readonly outputTokens: number;
46
+ readonly cacheReadInputTokens: number;
47
+ readonly unmeasuredCalls: number;
48
+ }
49
+ export interface RecordSummary {
50
+ readonly name: string | null;
51
+ readonly startedAt: number | null;
52
+ readonly endedAt: number | null;
53
+ readonly outcome: RecordOutcome | null;
54
+ readonly usage: RecordUsage;
55
+ readonly monitor: string | null;
56
+ readonly nodes: readonly RecordNodeSummary[];
57
+ readonly lines: readonly RecordLine[];
58
+ }
59
+ export interface RenderRecordOptions {
60
+ readonly lineWidth?: number;
61
+ }
62
+ export declare function summarizeRecord(events: readonly LoopEvent[]): RecordSummary;
63
+ export declare function formatDuration(ms: number): string;
64
+ export declare function renderRecord(events: readonly LoopEvent[], options?: RenderRecordOptions): string;
65
+ export declare function readRecordFile(path: string): {
66
+ events: LoopEvent[];
67
+ unreadable: number;
68
+ };
@@ -1,4 +1,4 @@
1
- export { INVALID_TEAM_DECISION, assertDistinctSeats, outcomeFromAgentText, panelReviewers, requireNoFiles, requireNonEmptyFiles, seatIdentity, teamAgent } from './chunk-DV5P4QLI.js';
1
+ export { INVALID_TEAM_DECISION, assertDistinctSeats, outcomeFromAgentText, panelReviewers, requireNoFiles, requireNonEmptyFiles, seatIdentity, teamAgent } from './chunk-ZXVFWRW5.js';
2
2
  import './chunk-NIBHM5I5.js';
3
3
  import './chunk-5GLEABOU.js';
4
4
  //# sourceMappingURL=workflow-support.js.map
@@ -1,5 +1,5 @@
1
1
  import { type TeamSeat } from '@obversa/api';
2
- import type { Job, Outcome, ConditionInput } from './core/types.js';
2
+ import type { Job, Judge, Outcome, ConditionInput } from './core/types.js';
3
3
  export interface BriefSource {
4
4
  readonly brief: string;
5
5
  readonly files?: readonly string[];
@@ -17,7 +17,13 @@ export interface WorkflowStageBase {
17
17
  readonly optional?: boolean;
18
18
  readonly needs?: string | readonly string[];
19
19
  readonly sendsBackTo?: string;
20
- readonly retry?: number;
20
+ /**
21
+ * How many more rounds a reviewed stage or a kickback target gets: a plain
22
+ * count, or `judge(seat, { cap, questions })` to let a seat decide between
23
+ * a review's verdict and the send-back, with `cap` as the hard backstop
24
+ * that stops it regardless of what the judge says.
25
+ */
26
+ readonly refine?: number | Judge;
21
27
  /** An interrupted attempt may run again without a person's reconciliation. */
22
28
  readonly retrySafe?: boolean;
23
29
  }
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@obversa/runtime",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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>",
7
7
  "type": "module",
8
8
  "main": "./dist/api.js",
9
+ "bin": {
10
+ "obversa-record": "./dist/bin/record.js"
11
+ },
9
12
  "types": "./dist/api.d.ts",
10
13
  "exports": {
11
14
  ".": {
@@ -48,16 +51,16 @@
48
51
  "dependencies": {
49
52
  "p-limit": "^7.3.0",
50
53
  "toposort": "^2.0.2",
51
- "@obversa/core": "0.1.0"
54
+ "@obversa/core": "0.2.0"
52
55
  },
53
56
  "peerDependencies": {
54
- "@obversa/api": "0.1.0"
57
+ "@obversa/api": "0.2.0"
55
58
  },
56
59
  "devDependencies": {
57
60
  "@types/node": "22.12.0",
58
61
  "@types/toposort": "^2.0.7",
59
62
  "execa": "^9.6.1",
60
- "@obversa/api": "0.1.0"
63
+ "@obversa/api": "0.2.0"
61
64
  },
62
65
  "keywords": [
63
66
  "agent",