Skip to content
Merged
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
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Requires `a3s-sandbox` 0.2.1, whose `NativeSandbox` is shared across
threads and applies network grants through `&self`.
- Workspace lexical FTS links published `a3s-vec` 0.1.8 (`a3s_vec_fts_v1`).
On-disk `zvec_rust_fts_v1` generations stay incompatible and are rebuilt.
- The fact log is the only coding control source. `send`, `stream`, attachment
Expand All @@ -53,7 +55,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Meta Harness arbitrary assemble (`META-HARNESS2`).** Ordered
`components: [...]` accepts stock parts and `host:<id>` mounts via
`HostHarnessRegistry` / `HostHarnessAssembler`. Node/Python expose
`Harness.host` + `components`. Builtin `intent_stamp` host component supports
`Harness.host` + `components`; Go exposes `SessionOptions.Harness`
(`HarnessOptions`, `HarnessHost`, stock part constants). SDK sessions that set
`harness` install `BuiltinHostHarnessRegistry`, so `host:<id>` mounts resolve
from Node, Python, and Go instead of failing closed for lack of a registry. Builtin `intent_stamp` host component supports
hermetic and Layer C proofs. F31 kernels (`meta_harness.rs`,
`completion_attestor.rs`) join the ≥95% F-table gate. Live suite:
`test_meta_harness_compose_live_e2e` (file/digest/gate oracles only).
Expand Down
36 changes: 21 additions & 15 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ a3s-search = { version = "=3.1.4", git = "https://github.com/A3S-Lab/Search.git"
# Flow engine is optional: thin coding builds do not project named workflows.
# Enabled by `dynamic-workflow` (and therefore `advanced-harness`).
a3s-flow = { version = "=1.1.0", git = "https://github.com/A3S-Lab/Flow.git", rev = "2948ad51a1395177764766c3ddf7e44338f9e374", default-features = false, optional = true }
a3s-sandbox = { version = "=0.2.0", git = "https://github.com/A3S-Lab/Sandbox.git", rev = "d381943061b157fb0e63973372d138cb6f78ffde" }
a3s-sandbox = { version = "=0.2.1", git = "https://github.com/A3S-Lab/Sandbox.git", rev = "2b8687892bbb4a5e3d6e74fca42949a3f186d236" }
# Typed System-1 decisions (Apofasi). Optional so thin coding builds stay
# ML-free; lexical/router path only unless `apofasi-infer` is enabled.
a3s-apofasi = { version = "=0.1.1", git = "https://github.com/A3S-Lab/Apofasi.git", rev = "0bc3961d3183f9d2692c0406967765866bc914d6", default-features = false, features = ["router"], optional = true }
Expand Down
24 changes: 21 additions & 3 deletions manual/META_HARNESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,27 @@ opts.harness = Harness.compose(
)
```

Unknown part names and unknown `host:<id>` values fail closed. `host:*` mounts
require `SessionOptions::with_host_harness_registry` (or the builtin registry in
tests).
```go
// Go
budget := uint32(4)
opts := &code.SessionOptions{
Harness: &code.HarnessOptions{
Components: []string{
code.HarnessSystem, code.HarnessTools, code.HarnessHost("intent_stamp"),
code.HarnessBudget, code.HarnessInfer,
},
ToolBudget: &budget,
System: []string{"You are a careful coding agent."},
},
}
```

Unknown part names fail closed when the session is created; unknown `host:<id>`
values fail closed on the first run. Rust embedders supply
`SessionOptions::with_host_harness_registry`. Node, Python, and Go sessions that
set `harness` install `BuiltinHostHarnessRegistry`, so SDK `host:<id>` mounts
resolve against Core's builtin components (currently `intent_stamp`); custom
host components need a Rust embedder.

## Verification

Expand Down
102 changes: 67 additions & 35 deletions scripts/sdk_api_alignment_check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,37 @@ function goStructFields(source, name) {
];
}

function goJsonTags(source, name) {
const match = source.match(
new RegExp(`^type\\s+${name}\\s+struct\\s*\\{([\\s\\S]*?)^\\}`, 'm'),
);
assert.ok(match, `could not find Go struct ${name}`);
// `json:"-"` fields travel over the callback transport; key them by the
// snake_case field name so they still count as present.
return [...match[1].matchAll(/^\s*([A-Z][A-Za-z0-9]*)\s+[^`\n]*`json:"([a-z0-9_-]+)/gm)].map(
([, field, tag]) =>
tag === '-' ? field.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase() : tag,
);
}

// Core/Node option name -> Go JSON key (`top` or `top.nested`) where Go groups
// related options into one typed struct instead of flat fields.
const GO_SESSION_OPTION_ALIASES = new Map([
['role', 'prompt_slots.role'],
['guidelines', 'prompt_slots.guidelines'],
['response_style', 'prompt_slots.response_style'],
['output_language', 'prompt_slots.output_language'],
['extra', 'prompt_slots.extra'],
['trajectory_path', 'trajectory.path'],
['trajectory_mode', 'trajectory.mode'],
['trajectory_max_text_bytes', 'trajectory.max_text_bytes'],
['trajectory_include_messages', 'trajectory.include_messages'],
['memory_store', 'file_memory_dir'],
['session_store', 'file_session_store_dir'],
['auto_parallel', 'auto_parallel_delegation'],
['planning', 'planning_mode'],
]);

function expected(coreMethods, omissions, aliases) {
return [
...new Set(
Expand Down Expand Up @@ -706,42 +737,43 @@ assertContainsAll('Go Session', goSession, [
'OutcomeLedgerSnapshot',
]);
assert.ok(!goSession.includes('ParallelTask'), 'Go Session must not expose ParallelTask (HARNESS-CONV4)');
assertContainsAll('Go SessionOptions', goSessionOptions, [
'Model',
'AgentDirs',
'SkillDirs',
'SearchConfig',
'FileMemoryDir',
'FileSessionStoreDir',
'SecurityProvider',
'SessionID',
'TenantID',
'Principal',
'AgentTemplateID',
'CorrelationID',
'PlanningMode',
'GoalTracking',
'AutoSave',
'ToolTimeoutMS',
'LLMAPITimeoutMS',
'AutoCompact',
'MaxContextTokens',
'Temperature',
'ThinkingBudget',
'MaxToolRounds',
'MaxParallelTasks',
'PromptSlots',
'ImmutableContentAdapter',
'CommandEnv',
'CompletionWaivers',
'EffectIsolation',
'ExternalObservations',
'OutcomeLedger',
'PathRules',
'PlanRun',
'ReadOnlySession',
'VerifierEnabled',
// Go must cover the same Core-derived surface as Node/Python. Every required
// SessionOptions name resolves to a Go JSON key, either top-level or inside the
// nested struct Go groups it under; a new Core field fails here until Go maps it.
const goSessionOptionKeys = new Set(goJsonTags(go, 'SessionOptions'));
const goNestedKeys = {
prompt_slots: new Set(goJsonTags(go, 'PromptSlots')),
trajectory: new Set(goJsonTags(go, 'TrajectoryConfig')),
};
const missingGoSessionOptions = requiredSessionOptions.filter((name) => {
const alias = GO_SESSION_OPTION_ALIASES.get(name) ?? name;
const [top, nested] = alias.split('.');
if (!goSessionOptionKeys.has(top)) return true;
return nested !== undefined && !goNestedKeys[top]?.has(nested);
});
assert.deepEqual(
missingGoSessionOptions,
[],
`Go SessionOptions is missing Core options: ${missingGoSessionOptions.join(', ')}`,
);
const normalizeGo = (name) => name.replace(/_/g, '').toLowerCase();
// Go idioms: the constructor is a package function, and context-taking calls
// block, so `*_async` variants collapse into the synchronous Go method.
const GO_METHOD_ALIASES = new Map([
['create', 'NewAgent'],
['replace_session_async', 'ReplaceSession'],
]);
const goConstructors = [...go.matchAll(/^func\s+(New[A-Z][A-Za-z0-9]*)\s*\(/gm)].map((m) => m[1]);
for (const [label, goNames, required] of [
['Go Agent', [...goAgent, ...goConstructors], requiredAgent],
['Go Session', goSession, requiredSession],
]) {
const present = new Set(goNames.map(normalizeGo));
const missing = required.filter(
(name) => !present.has(normalizeGo(GO_METHOD_ALIASES.get(name) ?? name)),
);
assert.deepEqual(missing, [], `${label} is missing Core methods: ${missing.join(', ')}`);
}
assertContainsAll('Go StateGraphRuntime', goMethods(go, 'StateGraphRuntime'), [
'BranchID',
'Version',
Expand Down
33 changes: 33 additions & 0 deletions sdk/go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,39 @@ profile drift, and child runs inherit the parent profile without broadening it.
provider through a typed object. The older `DefaultSecurity` boolean remains
available for wire compatibility but is deprecated; do not set both options.

## Meta Harness composition

Compose the harness from ordered components on the one fact log, the same
recipe Node's and Python's `Harness.compose` send:

```go
budget := uint32(4)
session, err := agent.Session(ctx, ".", &code.SessionOptions{
Harness: &code.HarnessOptions{
Components: []string{
code.HarnessSystem,
code.HarnessTools,
code.HarnessHost("intent_stamp"),
code.HarnessBudget,
code.HarnessInfer,
},
ToolBudget: &budget,
System: []string{"You are a careful coding agent."},
},
})
if err != nil {
return err
}
defer session.Close(context.Background())
```

Leave `Harness` nil to keep the default `coding_actor` tree. `HarnessHost`
mounts one of Core's builtin host components (currently `intent_stamp`);
custom host components need a Rust embedder with its own `HostHarnessRegistry`.
Unknown stock parts are rejected when the session is created, and an
unregistered host id fails closed on the first run. Permission projection and
the completion gate stay Core-owned and cannot be disabled here.

## Streaming

Every event uses the shared, lossless `EventEnvelopeV1` shape. `Event.Type` is
Expand Down
24 changes: 24 additions & 0 deletions sdk/go/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,30 @@ func TestToolPresentationProfileUsesTypedSessionOption(t *testing.T) {
}
}

func TestHarnessOptionsMatchBridgeWire(t *testing.T) {
budget := uint32(4)
encoded, err := json.Marshal(SessionOptions{
Harness: &HarnessOptions{
Components: []string{
HarnessSystem, HarnessTools, HarnessHost("intent_stamp"), HarnessBudget, HarnessInfer,
},
ToolBudget: &budget,
System: []string{"careful coding agent"},
},
})
if err != nil {
t.Fatal(err)
}
want := `{"harness":{"components":["system","tools","host:intent_stamp","budget","infer"],"tool_budget":4,"system":["careful coding agent"]}}`
if string(encoded) != want {
t.Fatalf("harness JSON = %s, want %s", encoded, want)
}
omitted, err := json.Marshal(SessionOptions{})
if err != nil || string(omitted) != `{}` {
t.Fatalf("omitted harness must keep the default tree, got %s, %v", omitted, err)
}
}

func TestCreateSessionAndCloseWithInjectedRuntime(t *testing.T) {
runtime := &fakeRuntime{
request: func(
Expand Down
1 change: 1 addition & 0 deletions sdk/go/bridge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ tokio = { version = "1.35", features = [
tokio-util = "0.7"

[dev-dependencies]
a3s-effect = "0.1.0"
tempfile = "3.10"

[features]
Expand Down
Loading
Loading