Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/execution-history-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@executor-js/plugin-execution-history": minor
---

Add an optional execution-history plugin that persists execution, tool-call,
and interaction lifecycle events through owner-scoped plugin storage.
18 changes: 18 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/plugins/execution-history/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @executor-js/plugin-execution-history
28 changes: 28 additions & 0 deletions packages/plugins/execution-history/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@executor-js/plugin-execution-history",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
".": "./src/sdk/index.ts"
},
"scripts": {
"build": "tsup && (tsc --declaration --emitDeclarationOnly --outDir dist --rootDir src || true)",
"typecheck": "tsgo --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"typecheck:slow": "tsc --noEmit"
},
"dependencies": {
"@executor-js/sdk": "workspace:*",
"effect": "catalog:"
},
"devDependencies": {
"@effect/vitest": "catalog:",
"@types/node": "catalog:",
"bun-types": "catalog:",
"tsup": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
}
}
109 changes: 109 additions & 0 deletions packages/plugins/execution-history/src/sdk/collections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Schema } from "effect";

import { definePluginStorageCollection } from "@executor-js/sdk/core";

// ---------------------------------------------------------------------------
// Execution-history storage collections.
//
// Three owner-scoped plugin-storage collections back the run history: one row
// per execution (`runs`), per tool call (`toolCalls`), and per interaction
// (`interactions`). Every payload that the engine hands us as `unknown` (tool
// args/results, interaction payloads/responses, execution results/logs) is
// stored as an already-serialized JSON string in a `*Json` column so the
// indexed columns stay primitive and query-friendly. Indexes are declared so
// the read surface can filter/sort on them (the facade type-enforces that only
// declared fields appear in `where`/`orderBy`).
// ---------------------------------------------------------------------------

/** Terminal + transient lifecycle state of a single execution. */
export const RunStatus = Schema.Literals([
"running",
"waiting_for_interaction",
"completed",
"failed",
]);
export type RunStatus = typeof RunStatus.Type;

/** Lifecycle state of a single tool call within an execution. */
export const ToolCallStatus = Schema.Literals(["running", "completed", "failed"]);
export type ToolCallStatus = typeof ToolCallStatus.Type;

/** Lifecycle state of a single interaction (elicitation) within an execution. */
export const InteractionStatus = Schema.Literals([
"pending",
"accepted",
"declined",
"cancelled",
"failed",
]);
export type InteractionStatus = typeof InteractionStatus.Type;

export const RunRow = Schema.Struct({
executionId: Schema.String,
status: RunStatus,
code: Schema.String,
resultJson: Schema.NullOr(Schema.String),
errorText: Schema.NullOr(Schema.String),
logsJson: Schema.NullOr(Schema.String),
triggerKind: Schema.NullOr(Schema.String),
triggerMetaJson: Schema.NullOr(Schema.String),
startedAt: Schema.Number,
completedAt: Schema.NullOr(Schema.Number),
durationMs: Schema.NullOr(Schema.Number),
toolCallCount: Schema.Number,
hadInteraction: Schema.Boolean,
});
export type RunRow = typeof RunRow.Type;

export const runs = definePluginStorageCollection("runs", RunRow, {
indexes: ["status", "triggerKind", "startedAt", "durationMs", "hadInteraction"],
});

export const ToolCallRow = Schema.Struct({
executionId: Schema.String,
toolCallId: Schema.String,
status: ToolCallStatus,
path: Schema.String,
namespace: Schema.NullOr(Schema.String),
argsJson: Schema.NullOr(Schema.String),
resultJson: Schema.NullOr(Schema.String),
errorText: Schema.NullOr(Schema.String),
startedAt: Schema.Number,
completedAt: Schema.NullOr(Schema.Number),
durationMs: Schema.NullOr(Schema.Number),
});
export type ToolCallRow = typeof ToolCallRow.Type;

export const toolCalls = definePluginStorageCollection("toolCalls", ToolCallRow, {
indexes: ["executionId", "startedAt"],
});

export const InteractionRow = Schema.Struct({
executionId: Schema.String,
interactionId: Schema.String,
status: InteractionStatus,
kind: Schema.String,
purpose: Schema.NullOr(Schema.String),
payloadJson: Schema.NullOr(Schema.String),
responseJson: Schema.NullOr(Schema.String),
errorText: Schema.NullOr(Schema.String),
startedAt: Schema.Number,
completedAt: Schema.NullOr(Schema.Number),
});
export type InteractionRow = typeof InteractionRow.Type;

export const interactions = definePluginStorageCollection("interactions", InteractionRow, {
indexes: ["executionId", "startedAt"],
});

export const TerminalOutboxRow = Schema.Struct({
executionId: Schema.String,
});
export type TerminalOutboxRow = typeof TerminalOutboxRow.Type;

/** Cleanup markers for terminal outbox blobs. The marker is committed in the
* same batch as the terminal run, then removed after its blob is deleted. */
export const terminalOutboxes = definePluginStorageCollection(
"terminalOutboxes",
TerminalOutboxRow,
);
22 changes: 22 additions & 0 deletions packages/plugins/execution-history/src/sdk/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export { executionHistoryPlugin } from "./plugin";

export {
interactions,
runs,
toolCalls,
InteractionRow,
InteractionStatus,
RunRow,
RunStatus,
ToolCallRow,
ToolCallStatus,
} from "./collections";

export {
makeExecutionHistoryObserver,
makeExecutionHistoryStore,
type ExecutionHistoryDetail,
type ExecutionHistoryListOptions,
type ExecutionHistoryListResult,
type ExecutionHistoryStore,
} from "./store";
32 changes: 32 additions & 0 deletions packages/plugins/execution-history/src/sdk/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { definePlugin } from "@executor-js/sdk/core";

import { interactions, runs, terminalOutboxes, toolCalls } from "./collections";
import { makeExecutionHistoryObserver, makeExecutionHistoryStore } from "./store";

// ---------------------------------------------------------------------------
// Execution-history plugin (SDK surface). A pure sink: it contributes no tools
// or integrations, only the three storage collections, a read surface
// (`list`/`get`/`listToolCalls`) on `executor.executionHistory`, and a runtime
// ExecutionObserver that records the engine's event stream.
//
// One store instance is shared: `storage(deps)` builds it (read methods +
// buffered `handleEvent` writer), `extension(ctx)` surfaces the read methods
// AND `handleEvent` off `ctx.storage`, and `runtime.executionObserver(self)`
// (which receives the EXTENSION) wraps `self.handleEvent` into an observer.
// ---------------------------------------------------------------------------

export const executionHistoryPlugin = definePlugin(() => ({
id: "executionHistory" as const,
packageName: "@executor-js/plugin-execution-history",
pluginStorage: { runs, toolCalls, interactions, terminalOutboxes },
storage: (deps) => makeExecutionHistoryStore(deps),
extension: (ctx) => ({
list: ctx.storage.list,
get: ctx.storage.get,
listToolCalls: ctx.storage.listToolCalls,
handleEvent: ctx.storage.handleEvent,
}),
runtime: {
executionObserver: (self) => makeExecutionHistoryObserver(self),
},
}));
Loading
Loading