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
10 changes: 10 additions & 0 deletions .changeset/steady-agents-connect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@sapiom/harness": patch
---

Adapt direct source invocations into package-scoped graph evidence with
content-based freshness, opaque callsite references, conservative coverage,
and last-good replacement semantics while preserving the existing System Graph
edge, warning, and settled-cache behavior. Dynamic targets remain explicit
partial evidence without making an otherwise complete deterministic source scan
retryable.
36 changes: 31 additions & 5 deletions packages/harness/docs/workspace-system-graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,29 @@ agent outputs through formatter/helper/router code, or scan arbitrary workspace
router modules outside those roots. Cross-agent output-to-input analysis will
use a separate package-level evidence provider.

The caller-scoped scanner remains an internal extraction boundary. After the
scanner returns, the Harness resolves each literal target against the selected
package inventory and adapts `(caller.agentKey, resolvedTarget.agentKey)` into
an explicit `invokes` / `static-invocation` package evidence record. That
record is scoped to the exact inventory version and carries the typed call mode,
producer/version, and an analysis fingerprint. Source locations become opaque
`source-callsite` references; paths remain server-side. The public
`StaticInvocationGraphEdge` above is derived from accepted evidence and is not
used as the evidence store.

Evidence freshness hashes the bounded source content that was actually
analyzed. Watcher paths, mtimes, `observedPaths`, UI node IDs, path slugs, and
the cheap `fingerprintWorkflowSources` cache key do not become canonical
evidence identity. Every settled bounded refresh atomically replaces the prior
proven literal subset, including retracting removed calls. Dynamic targets or
structural limits keep topology coverage explicitly partial without preventing
that settled subset from refreshing. A dynamic-target limitation alone remains
cacheable; a structurally incomplete scan remains non-cacheable and retryable.
Pending, failed, missing, or inconsistent caller scans instead retain the prior
accepted subset and diagnose the latest attempt. The initial cache-only phase
does not seed last-good evidence, so the first settled partial scan can expose
its proven static edges.

Projection can remain useful while reporting warnings:

| Warning code | Meaning |
Expand Down Expand Up @@ -160,11 +183,14 @@ while the snapshot is `ready`; an incomplete workspace walk, pending/retryable
identity, or incomplete invocation scan keeps it `degraded`. Warnings and the
Retry affordance remain visible without freezing evidence that may still change.

Package inventory protocol 1 is deliberately limited to which agents exist,
their stable identities, and their package-relative locations. It carries no
agent-owned or opaque relationship payload. Future package-wide data-flow and
cross-agent relationship evidence will use a separate versioned contract with
its own deterministic provenance and validation.
Package inventory protocol 1 remains deliberately limited to which agents
exist, their stable identities, and their package-relative locations. It
carries no agent-owned or opaque relationship payload. Relationship producers
instead use the separately versioned package graph-evidence protocol, bound to
one exact inventory version with deterministic provenance and validation.
Future package-wide data-flow analysis can produce `feeds` /
`static-dataflow` evidence through that contract without broadening this
direct-invocation scanner.

## Freshness event

Expand Down
35 changes: 33 additions & 2 deletions packages/harness/src/core/canvas-interconnections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* calls are syntax-accurate (comments and strings cannot become invocations),
* while dynamic targets are returned as explicit extraction warnings.
*/
import { createHash } from "node:crypto";
import { constants as fsConstants } from "node:fs";
import * as fs from "node:fs/promises";
import * as path from "node:path";
Expand Down Expand Up @@ -539,6 +540,8 @@ export interface WorkflowSourceScan {
observedPaths: string[];
/** False when an opaque path or work cap prevented a complete scan. */
complete: boolean;
/** Stable identity of the source content supplied to this extraction. */
sourceFingerprint: `sha256:${string}`;
}

interface SupportedNamespaces {
Expand Down Expand Up @@ -885,9 +888,20 @@ export async function scanWorkflowSources(
const invocationWarnings: AgentInvocationDetectionWarning[] = [];
const capabilities: DetectedCapability[] = [];
const sourceSet = await listSourceFilesWithObservations(root);
const fingerprintInputs: Array<{
file: string;
contentDigest: `sha256:${string}` | null;
}> = [];
let complete = sourceSet.complete;
for (const file of sourceSet.files) {
const content = await readWorkflowSourceFile(root, file, readHooks);
fingerprintInputs.push({
file: path.relative(root, file).split(path.sep).join(path.posix.sep),
contentDigest:
content === null
? null
: `sha256:${createHash("sha256").update(content).digest("hex")}`,
});
if (content === null) {
complete = false;
continue;
Expand Down Expand Up @@ -923,18 +937,35 @@ export async function scanWorkflowSources(
const launches = invocations
.filter((invocation) => invocation.mode === "async")
.map(({ slug, fromStepId }) => ({ slug, fromStepId }));
fingerprintInputs.sort((left, right) =>
left.file === right.file ? 0 : left.file < right.file ? -1 : 1,
);
const sourceFingerprint = `sha256:${createHash("sha256")
.update(
JSON.stringify({
protocol: 1,
complete,
sources: fingerprintInputs,
}),
)
.digest("hex")}` as const;
return {
launches,
invocations,
invocationWarnings,
capabilities,
observedPaths: sourceSet.observedPaths,
complete,
sourceFingerprint,
};
}

/** Direct agent invocations plus deterministic warnings for supported calls
* whose target is not a direct literal. */
/**
* Test-only compatibility helper for direct invocation extraction. Production
* callers use `SourceAgentInvocationProvider`, which consumes the shared scan.
*
* @internal
*/
export async function detectAgentInvocations(
root: string,
knownStepIds: ReadonlySet<string>,
Expand Down
Loading
Loading