@inflexa-ai/harness 0.44.0 → 0.45.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.
Files changed (99) hide show
  1. package/dist/app/approve-plan.d.ts +48 -0
  2. package/dist/app/approve-plan.js +84 -0
  3. package/dist/app/data-profile-view.d.ts +1 -1
  4. package/dist/app/run-activity.js +4 -6
  5. package/dist/contracts/chat-events.d.ts +2 -0
  6. package/dist/contracts/chat-frame.d.ts +7 -0
  7. package/dist/contracts/chat-frame.js +18 -4
  8. package/dist/contracts/chat-parts.d.ts +73 -2
  9. package/dist/contracts/chat-parts.js +3 -1
  10. package/dist/contracts/index.d.ts +3 -1
  11. package/dist/contracts/index.js +1 -0
  12. package/dist/contracts/part-registry.js +3 -0
  13. package/dist/contracts/schemas/chat-events.d.ts +13 -0
  14. package/dist/contracts/schemas/chat-events.js +1 -0
  15. package/dist/contracts/schemas/chat-parts.d.ts +200 -15
  16. package/dist/contracts/schemas/chat-parts.js +63 -12
  17. package/dist/contracts/schemas/index.d.ts +1 -1
  18. package/dist/contracts/schemas/index.js +1 -1
  19. package/dist/contracts/subagent-activity.d.ts +12 -0
  20. package/dist/contracts/subagent-activity.js +76 -0
  21. package/dist/execution/artifact-metadata.js +1 -3
  22. package/dist/execution/run-canceler.js +1 -1
  23. package/dist/execution/run-event-parts.d.ts +6 -5
  24. package/dist/execution/run-event-parts.js +6 -5
  25. package/dist/execution/run-event-stream.d.ts +18 -6
  26. package/dist/execution/run-event-stream.js +67 -17
  27. package/dist/execution/run-synthesis.d.ts +21 -0
  28. package/dist/execution/run-synthesis.js +77 -3
  29. package/dist/index.d.ts +22 -8
  30. package/dist/index.js +16 -2
  31. package/dist/lib/hooks.d.ts +1 -1
  32. package/dist/lib/metrics.d.ts +2 -2
  33. package/dist/loop/run-agent.js +1 -0
  34. package/dist/loop/types.d.ts +2 -0
  35. package/dist/memory/card-builders.d.ts +11 -0
  36. package/dist/memory/card-builders.js +35 -21
  37. package/dist/memory/conversation-display-recorder.d.ts +2 -0
  38. package/dist/memory/conversation-display-recorder.js +10 -1
  39. package/dist/memory/conversation-display-storage.d.ts +9 -2
  40. package/dist/memory/conversation-display-storage.js +18 -3
  41. package/dist/memory/thread-history.d.ts +8 -1
  42. package/dist/memory/thread-history.js +13 -3
  43. package/dist/memory/thread-store.d.ts +13 -0
  44. package/dist/memory/thread-store.js +15 -1
  45. package/dist/prompts/conversation.d.ts +1 -1
  46. package/dist/prompts/conversation.js +3 -1
  47. package/dist/provenance/seam.d.ts +16 -5
  48. package/dist/providers/ai-sdk.js +6 -4
  49. package/dist/report-model/pin-snapshot.d.ts +1 -1
  50. package/dist/report-model/pin-snapshot.js +2 -50
  51. package/dist/runtime/assemble.d.ts +8 -1
  52. package/dist/runtime/assemble.js +11 -0
  53. package/dist/runtime/boot.d.ts +3 -0
  54. package/dist/runtime/boot.js +10 -1
  55. package/dist/sandbox/sandbox-step-translate.d.ts +23 -0
  56. package/dist/sandbox/sandbox-step-translate.js +24 -0
  57. package/dist/schemas/artifact-manifest.d.ts +1 -1
  58. package/dist/schemas/artifact-manifest.js +11 -9
  59. package/dist/schemas/plan-schemas.d.ts +18 -18
  60. package/dist/schemas/run-synthesis.d.ts +2 -2
  61. package/dist/state/analyses-metrics.d.ts +79 -7
  62. package/dist/state/analyses-metrics.js +141 -6
  63. package/dist/state/artifacts.d.ts +50 -3
  64. package/dist/state/artifacts.js +67 -7
  65. package/dist/state/data-profile.d.ts +4 -2
  66. package/dist/state/data-profile.js +7 -5
  67. package/dist/state/index.d.ts +9 -8
  68. package/dist/state/index.js +5 -5
  69. package/dist/state/migrations/20261012120000_index_the_runs_by_start.d.ts +8 -0
  70. package/dist/state/migrations/20261012120000_index_the_runs_by_start.js +12 -0
  71. package/dist/state/migrations/20261013120000_index_the_ended_runs_and_the_recent_threads.d.ts +8 -0
  72. package/dist/state/migrations/20261013120000_index_the_ended_runs_and_the_recent_threads.js +13 -0
  73. package/dist/state/migrations/20261013180000_narrow_the_active_run_index.d.ts +8 -0
  74. package/dist/state/migrations/20261013180000_narrow_the_active_run_index.js +13 -0
  75. package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.d.ts +8 -0
  76. package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.js +13 -0
  77. package/dist/state/migrations/index.js +8 -0
  78. package/dist/state/plans.d.ts +21 -0
  79. package/dist/state/plans.js +25 -0
  80. package/dist/state/runs.d.ts +36 -5
  81. package/dist/state/runs.js +65 -19
  82. package/dist/state/schema.d.ts +2 -0
  83. package/dist/tasks/data-profile-activity.d.ts +7 -1
  84. package/dist/tasks/data-profile-activity.js +18 -9
  85. package/dist/tasks/data-profile.d.ts +15 -1
  86. package/dist/tasks/data-profile.js +16 -11
  87. package/dist/tools/approval/gateway.js +28 -5
  88. package/dist/tools/display/show-user.d.ts +1 -1
  89. package/dist/tools/execute-analysis.d.ts +51 -0
  90. package/dist/tools/execute-analysis.js +241 -109
  91. package/dist/tools/research/inspect-data-profile.d.ts +1 -1
  92. package/dist/tools/research/inspect-run.d.ts +1 -1
  93. package/dist/tools/research/inspect-run.js +7 -7
  94. package/dist/workflows/execute-analysis.d.ts +4 -3
  95. package/dist/workflows/execute-analysis.js +35 -22
  96. package/dist/workflows/plan/graph.d.ts +16 -0
  97. package/dist/workflows/plan/graph.js +36 -0
  98. package/dist/workflows/sandbox-step.js +14 -12
  99. package/package.json +1 -1
@@ -296,7 +296,7 @@ export async function runDataProfileBody(input, deps) {
296
296
  // ledger has no suspended state and the retry path takes a `failed` row again.
297
297
  const suspendProfile = async (suspension) => {
298
298
  logger.warn("profile suspended", { reason: suspension.reason });
299
- settleTerminalWrite(logger, analysisId, unwrapOrThrow(await failDataProfile(deps.pool, analysisId, suspension.reason)), "failed");
299
+ await settleTerminalWrite(logger, analysisId, unwrapOrThrow(await failDataProfile(deps.pool, analysisId, suspension.reason)), "failed", deps.notifyDataProfileSettled);
300
300
  await revoke("data-profile-suspended");
301
301
  unwrapOrThrow(await suspendAnalysis(deps.pool, analysisId));
302
302
  await activity.failed(suspension.reason);
@@ -310,7 +310,7 @@ export async function runDataProfileBody(input, deps) {
310
310
  // this capability, and a body that composed its own strings is a body they can drift from.
311
311
  // The frame is the workflow's synthetic one: both values are constants shared by every analysis,
312
312
  // so they identify nothing and consumers are required not to key on them.
313
- const activity = createProfileActivityEmitter((part) => DBOS.writeStream("events", part), { runId: DATA_PROFILE_RUN_LITERAL, stepId: DATA_PROFILE_STEP_LITERAL }, logger);
313
+ const activity = createProfileActivityEmitter((part) => DBOS.writeStream("events", part), { runId: DATA_PROFILE_RUN_LITERAL, stepId: DATA_PROFILE_STEP_LITERAL }, logger, () => DBOS.now());
314
314
  try {
315
315
  // Record which workflow owns this profile before anything else, so a consumer that resolves
316
316
  // the id from the ledger can subscribe from the earliest possible moment. Guarded on a real
@@ -337,7 +337,7 @@ export async function runDataProfileBody(input, deps) {
337
337
  // catalogue's fit is measured, and a completion that emitted none would make the
338
338
  // empty-manifest path invisible to the same monitoring that watches every other.
339
339
  logProfileMonitoring(logger, { dimensions: [], probes: [], repairRounds: 0, absorb: "none" });
340
- settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId)), "completed");
340
+ await settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId)), "completed", deps.notifyDataProfileSettled);
341
341
  await revoke("data-profile-completed");
342
342
  // This is a terminal completion like any other, so it reports one — a consumer watching
343
343
  // an empty-manifest profile sees it settle rather than seeing the stream simply stop.
@@ -390,7 +390,7 @@ export async function runDataProfileBody(input, deps) {
390
390
  await activity.indexing();
391
391
  await indexProfile({ deps, analysisId, session: runSession, record, scan, logger });
392
392
  logProfileMonitoring(logger, { resolution, dimensions: [], probes: [], repairRounds: 0, absorb: "none" });
393
- settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, record)), "completed");
393
+ await settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, record)), "completed", deps.notifyDataProfileSettled);
394
394
  await revoke("data-profile-completed");
395
395
  await activity.complete();
396
396
  return;
@@ -416,7 +416,7 @@ export async function runDataProfileBody(input, deps) {
416
416
  repairRounds: 0,
417
417
  absorb: absorb.kind,
418
418
  });
419
- settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, absorbed)), "completed");
419
+ await settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, absorbed)), "completed", deps.notifyDataProfileSettled);
420
420
  await revoke("data-profile-completed");
421
421
  await activity.complete();
422
422
  return;
@@ -571,7 +571,7 @@ export async function runDataProfileBody(input, deps) {
571
571
  });
572
572
  // 6. Complete — store the FULL profiler finding plus the input signature for
573
573
  // staleness detection. The scratch tree is gone; this row is all that survives.
574
- settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, profileRecord)), "completed");
574
+ await settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, profileRecord)), "completed", deps.notifyDataProfileSettled);
575
575
  await revoke("data-profile-completed");
576
576
  // LAST statement of the success path, and that placement is the guarantee that exactly
577
577
  // one terminal activity is ever emitted. Everything that could still throw — the ledger
@@ -598,7 +598,7 @@ export async function runDataProfileBody(input, deps) {
598
598
  return await suspendProfile(suspension);
599
599
  logger.error("profile failed", logger.errorFields(err));
600
600
  const reason = profileFailureReason(err);
601
- settleTerminalWrite(logger, analysisId, unwrapOrThrow(await failDataProfile(deps.pool, analysisId, reason)), "failed");
601
+ await settleTerminalWrite(logger, analysisId, unwrapOrThrow(await failDataProfile(deps.pool, analysisId, reason)), "failed", deps.notifyDataProfileSettled);
602
602
  await revoke("data-profile-failed");
603
603
  // The same bounded, user-safe line the ledger receives — never the raw error, whose paths and
604
604
  // stack frames stay in the log record above. This is the only other terminal emission, so
@@ -617,18 +617,23 @@ function logTerminalNoop(logger, analysisId, write) {
617
617
  }
618
618
  /**
619
619
  * Settle one terminal write of the profile ledger. A refused stamp is logged
620
- * (see `logTerminalNoop`). An accepted stamp records the profile's run outcome:
621
- * the CAS admits one write per `running` claim, so this is the exactly-once
620
+ * (see `logTerminalNoop`) and sends no notice. An accepted stamp records the
621
+ * profile's run outcome and then delivers the settle notice when the deps bind
622
+ * one: the CAS admits one write per `running` claim, so this is the exactly-once
622
623
  * point of the transition — a recovery replay of the body finds the row no
623
- * longer `running`, and records nothing again. The duration runs from the
624
+ * longer `running`, and does neither again. The duration runs from the
624
625
  * `data_profile_started_at` the claim stamped, never from a body-local clock.
626
+ * A crash between the stamp and the notice loses that one notice.
625
627
  */
626
- function settleTerminalWrite(logger, analysisId, write, status) {
628
+ async function settleTerminalWrite(logger, analysisId, write, status, notify) {
627
629
  if (!write.stamped) {
628
630
  logTerminalNoop(logger, analysisId, status === "completed" ? "completion" : "failure");
629
631
  return;
630
632
  }
631
633
  recordRunCompleted({ workflow: "data_profile", status, durationMs: elapsedSinceIso(write.startedAt) });
634
+ if (notify === undefined)
635
+ return;
636
+ await deliverNotice(logger, "notifyDataProfileSettled", notify({ analysisId, status, completedAt: write.completedAt }));
632
637
  }
633
638
  /** A one-line, user-facing description of a `DataProfileStartError`. */
634
639
  export function describeDataProfileStartError(e) {
@@ -40,8 +40,11 @@ export function createAskGateway(deps) {
40
40
  // A standing grant this user holds for this ask's grant key — the command
41
41
  // unless the tool supplied a broader key — short-circuits the prompt: record
42
42
  // the invocation as `resolved` for the audit trail and return without pausing.
43
+ // The one part it emits is already settled, so no surface prompts, and the
44
+ // transcript still shows that a standing grant approved the call.
43
45
  if (unwrapOrThrow(await selectGrant(pool, ctx.analysisId, ctx.userId, request.grantKey ?? request.command))) {
44
46
  unwrapOrThrow(await insertGrantedAsk(pool, row, now));
47
+ await emitAskPart(ctx.emit, id, request, "resolved", "always");
45
48
  return { kind: "always" };
46
49
  }
47
50
  unwrapOrThrow(await insertPendingAsk(pool, row));
@@ -69,7 +72,7 @@ export function createAskGateway(deps) {
69
72
  throw new AskRejectedError("approval request is no longer available");
70
73
  }
71
74
  if (resolution.status !== "pending") {
72
- await emitAskPart(ctx.emit, id, request, resolution.status);
75
+ await emitAskPart(ctx.emit, id, request, resolution.status, replyKindOf(resolution.status, resolution.reply));
73
76
  return settle(resolution.status, resolution.reply, ctx.signal);
74
77
  }
75
78
  await sleepUntil(POLL_INTERVAL_MS, ctx.signal);
@@ -88,7 +91,7 @@ export function createAskGateway(deps) {
88
91
  return { ask, answer, pending, sweepExpired };
89
92
  }
90
93
  /** Emit (or reconcile, under the same id) the ask's `data-ask` chat part. */
91
- function emitAskPart(emit, id, request, status) {
94
+ function emitAskPart(emit, id, request, status, reply) {
92
95
  return emit({
93
96
  type: "data-ask",
94
97
  data: {
@@ -97,16 +100,36 @@ function emitAskPart(emit, id, request, status) {
97
100
  command: request.command,
98
101
  ...(request.detail !== undefined ? { detail: request.detail } : {}),
99
102
  status,
103
+ ...(reply !== undefined ? { reply } : {}),
100
104
  },
101
105
  });
102
106
  }
107
+ /**
108
+ * The kind of reply the terminal part shows: the kind that `settle` acts on for a `resolved` or `rejected` row,
109
+ * and none for any other status. It never reads the feedback of a rejection, which is for the model.
110
+ */
111
+ function replyKindOf(status, reply) {
112
+ switch (status) {
113
+ case "resolved":
114
+ return approvalOf(reply).kind;
115
+ case "rejected":
116
+ return "reject";
117
+ case "expired":
118
+ case "aborted":
119
+ return undefined;
120
+ }
121
+ }
122
+ /** The approval that a `resolved` row grants. */
123
+ function approvalOf(reply) {
124
+ // Only `once` / `always` reach `resolved`; guard the JSONB round-trip and
125
+ // default to the narrowest approval if a malformed reply ever slipped in.
126
+ return reply && (reply.kind === "once" || reply.kind === "always") ? reply : { kind: "once" };
127
+ }
103
128
  /** Resolve an approval or throw the terminal denial the ledger recorded. */
104
129
  function settle(status, reply, signal) {
105
130
  switch (status) {
106
131
  case "resolved":
107
- // Only `once` / `always` reach `resolved`; guard the JSONB round-trip and
108
- // default to the narrowest approval if a malformed reply ever slipped in.
109
- return reply && (reply.kind === "once" || reply.kind === "always") ? reply : { kind: "once" };
132
+ return approvalOf(reply);
110
133
  case "rejected":
111
134
  throw new AskRejectedError(reply && reply.kind === "reject" ? reply.feedback : undefined);
112
135
  case "expired":
@@ -26,7 +26,7 @@ type ShowUserOutput = {
26
26
  id: string;
27
27
  };
28
28
  export declare const showUserTool: import("../define-tool.js").Tool<{
29
- kind: "code" | "markdown" | "echart" | "svg" | "table" | "structure";
29
+ kind: "code" | "echart" | "markdown" | "svg" | "table" | "structure";
30
30
  title?: string | undefined;
31
31
  spec?: Record<string, unknown> | undefined;
32
32
  dataPath?: string | undefined;
@@ -2,7 +2,9 @@
2
2
  * Unified async edge for approved plans and explicitly requested one-step ad
3
3
  * hoc analyses. Both modes launch the ordinary executeAnalysis workflow.
4
4
  */
5
+ import { ResultAsync } from "neverthrow";
5
6
  import type { Pool } from "pg";
7
+ import type { AgentSession } from "../auth/types.js";
6
8
  import type { UsageRecorder } from "../billing/usage-recorder.js";
7
9
  import type { EnvironmentStorePaths } from "../config/environment-stores.js";
8
10
  import type { ResourcePolicy } from "../config/resource-limits.js";
@@ -54,7 +56,56 @@ export declare class PlanPackagesMissingError extends Error {
54
56
  readonly unavailable?: string | undefined;
55
57
  constructor(missing: string[], collisions: string[], unavailable?: string | undefined);
56
58
  }
59
+ /** The deps that start the run of a stored plan. */
60
+ export type LaunchPlanRunDeps = Pick<ExecuteAnalysisToolDeps, "pool" | "executeAnalysisWorkflow" | "runAuthorizer" | "runLauncher" | "resourcePolicy" | "extendAnalysisFarm" | "logger">;
61
+ /** The run of a stored plan. `started` is false when a run of the plan was already active. */
62
+ export type PlanLaunch = {
63
+ runId: string;
64
+ started: boolean;
65
+ };
66
+ /**
67
+ * Why a stored plan did not start a run. `store_failed.cause` and `launch_failed.cause` are the
68
+ * values as the failing call gave them, thus a caller that must throw them keeps their class.
69
+ */
70
+ export type LaunchPlanRunRefusal = {
71
+ type: "plan_not_found";
72
+ planId: string;
73
+ } | {
74
+ type: "plan_invalid";
75
+ planId: string;
76
+ malformed: boolean;
77
+ errors: string[];
78
+ } | {
79
+ type: "packages_missing";
80
+ missing: string[];
81
+ collisions: string[];
82
+ unavailable?: string;
83
+ } | {
84
+ type: "not_authorized";
85
+ reason: string;
86
+ } | {
87
+ type: "suspended";
88
+ reason: string;
89
+ } | {
90
+ type: "store_failed";
91
+ cause: unknown;
92
+ } | {
93
+ type: "launch_failed";
94
+ cause: unknown;
95
+ };
57
96
  export declare function buildAdHocPlan(request: string, route: AdHocRoute, createdAt?: string): AnalysisPlan;
97
+ /**
98
+ * Start the run of a stored plan. It reads the plan under the analysis of the session, validates it,
99
+ * links its packages, reserves a run, authorizes it, and starts the workflow. It gives the run id, or
100
+ * the refusal, and it emits no chat part. `started` is false when a run of the plan was already active.
101
+ *
102
+ * This is an API boundary: a host calls it outside a tool body, thus no outcome above rejects the
103
+ * promise.
104
+ */
105
+ export declare function launchPlanRun(deps: LaunchPlanRunDeps, input: {
106
+ session: AgentSession;
107
+ planId: string;
108
+ }): ResultAsync<PlanLaunch, LaunchPlanRunRefusal>;
58
109
  export declare function createExecuteAnalysisTool(deps: ExecuteAnalysisToolDeps): import("./define-tool.js").Tool<{
59
110
  mode: "plan" | "adhoc";
60
111
  planId?: string | undefined;