Skip to content

Commit d3ad5cc

Browse files
committed
Merge remote-tracking branch 'origin/main' into codex/pr-32-conflict-fix
Change-Id: Id30ec2e3b79bc4def6eba0fcf3d8707e68bd0fd3
2 parents 89cea09 + 414589e commit d3ad5cc

17 files changed

Lines changed: 390 additions & 447 deletions

File tree

.github/pull_request_template.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
## Behavior / risk
3737

3838
<!-- Required only when changing provider contracts, config/schema parsing, release/package files, or GitHub workflows.
39-
State the user-visible behavior or compatibility/security risk. “No user-visible behavior change” is valid for an internal refactor. -->
39+
State the user-visible behavior or compatibility/security risk. “No user-visible behavior change” is valid for an internal refactor.
40+
No special commit-message marker or history rewrite is required. -->
4041

4142
## Validation
4243

.github/workflows/ci.yml

Lines changed: 1 addition & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -50,73 +50,6 @@ jobs:
5050
BASE: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
5151
run: bun scripts/verify.ts full --step "${{ matrix.step }}"
5252

53-
policy-surface:
54-
name: Policy surface
55-
runs-on: ubuntu-latest
56-
steps:
57-
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
58-
with:
59-
fetch-depth: 0
60-
- name: Require [policy] marker when control files change
61-
env:
62-
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
63-
HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
64-
run: |
65-
set -euo pipefail
66-
67-
# The control plane: deterministic gates and the rules that govern them.
68-
# Editing any of these changes what every other check enforces, so the
69-
# change must be explicit — a [policy] marker in the commit range.
70-
patterns='
71-
.dependency-cruiser.cjs
72-
sgconfig.yml
73-
biome.json
74-
tools/architecture/
75-
.githooks/
76-
.claude/hooks/
77-
.github/workflows/
78-
scripts/lint-changed.sh
79-
scripts/verify.ts
80-
scripts/verify.test.ts
81-
scripts/pr-evidence.ts
82-
scripts/pr-evidence.test.ts
83-
scripts/release/
84-
scripts/setup-githooks.sh
85-
'
86-
87-
base="$BASE_SHA"
88-
# New branch / first push: no usable base; nothing to diff against.
89-
if [ -z "$base" ] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then
90-
echo "No base commit to compare against; skipping policy-surface check."
91-
exit 0
92-
fi
93-
94-
changed=$(git diff --name-only "$base" "$HEAD_SHA")
95-
touched=""
96-
for p in $patterns; do
97-
hit=$(printf '%s\n' "$changed" | grep -F "$p" || true)
98-
if [ -n "$hit" ]; then
99-
touched="$touched$hit"$'\n'
100-
fi
101-
done
102-
103-
if [ -z "$touched" ]; then
104-
echo "No control-plane files changed."
105-
exit 0
106-
fi
107-
108-
echo "Control-plane files changed:"
109-
printf '%s' "$touched" | sed 's/^/ - /'
110-
111-
if git log "$base..$HEAD_SHA" --format=%B | grep -qF '[policy]'; then
112-
echo "Found [policy] marker in commit range. OK."
113-
exit 0
114-
fi
115-
116-
echo "::error::Control-plane files changed without a [policy] marker in any commit message."
117-
echo "Add '[policy]' to a commit message to acknowledge changing the verification harness itself."
118-
exit 1
119-
12053
maintainer-evidence:
12154
name: Maintainer evidence
12255
runs-on: ubuntu-latest
@@ -157,14 +90,13 @@ jobs:
15790
gate:
15891
name: Gate
15992
runs-on: ubuntu-latest
160-
needs: [prepare-verification, verify, policy-surface, maintainer-evidence, package-compatibility]
93+
needs: [prepare-verification, verify, maintainer-evidence, package-compatibility]
16194
if: always()
16295
steps:
16396
- name: Check results
16497
run: |
16598
if [ "${{ needs.prepare-verification.result }}" != "success" ] || \
16699
[ "${{ needs.verify.result }}" != "success" ] || \
167-
[ "${{ needs.policy-surface.result }}" != "success" ] || \
168100
[ "${{ needs.maintainer-evidence.result }}" != "success" ] || \
169101
[ "${{ needs.package-compatibility.result }}" != "success" ]; then
170102
echo "One or more required checks failed."

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Every PR must satisfy:
3939
2. At least one maintainer approval
4040
3. No unresolved review comments
4141

42-
We keep the contribution path open: documentation, examples, and ordinary small changes do not need a design document or a special PR label. Changes to provider contracts, configuration/schema parsing, release/package files, or GitHub workflows have a small additional requirement: fill in the **Behavior / risk** and **Validation** sections of the PR description. This lets maintainers review the contract and its evidence before reading an implementation diff.
42+
We keep the contribution path open: documentation, examples, and ordinary small changes do not need a design document, a special PR label, or a commit-message marker. Changes to provider contracts, configuration/schema parsing, release/package files, GitHub workflows, or verification policy have a small additional requirement: fill in the **Behavior / risk** and **Validation** sections of the PR description. This lets maintainers review the contract and its evidence before reading an implementation diff without asking contributors to rewrite commit history. A generated `bun.lock` change by itself is handled by audit and compatibility checks.
4343

4444
Maintainers should enable the repository settings that make the same policy effective at merge time: require the `Gate` and `Analyze TypeScript` checks, require one approving review, dismiss stale approvals, require approval of the latest push, and require resolved conversations. CODEOWNERS review and a merge queue are intentionally not required for routine contributions at the current project stage.
4545

apps/server/src/services/sessions/playbook-session-adapter/index.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,15 @@
1-
import {
2-
createPlaybookSessionRuntime,
3-
getPlaybookAppId,
4-
getSeedPlaybookAgentName,
5-
PLAYBOOK_AGENT_NAME_PREFIX,
6-
type RemotePlaybookAgent,
7-
} from "@openagentpack/playbooks";
1+
import { getPlaybookAppId, getSeedPlaybookAgentName, PLAYBOOK_AGENT_NAME_PREFIX } from "@openagentpack/playbooks";
82
import {
93
type CloudAgent,
104
listCloudAgents,
11-
type ProviderSessionEvent,
125
readProjectRuntime,
136
resolveSessionProvider,
14-
type Session,
157
startSessionRun,
168
} from "@openagentpack/sdk";
179
import { loadAgentRuntimeInput, withAgentRuntime } from "@/services/runtime-factory";
1810
import { agentMetadataOf } from "./dto";
1911
import { ensureAgentApplied } from "./provision";
12+
import { createPlaybookSessionRuntime, type RemotePlaybookAgent } from "./runtime";
2013
import {
2114
attachLiveStream,
2215
deletePlaybookSession,
@@ -29,7 +22,7 @@ import {
2922
export type { PlaybookSessionDetail } from "./sessions";
3023

3124
export function createServerPlaybookSessionRuntime() {
32-
return createPlaybookSessionRuntime<PlaybookSessionDetail, ProviderSessionEvent, Session, RemotePlaybookAgent>({
25+
return createPlaybookSessionRuntime({
3326
identity: {
3427
appId: getPlaybookAppId(),
3528
expectedAgentName: getSeedPlaybookAgentName,
@@ -87,12 +80,10 @@ export function createServerPlaybookSessionRuntime() {
8780
onDuplicateAgent({ playbookId, winner, duplicates }) {
8881
const all = [winner, ...duplicates];
8982
console.warn(
90-
`玩法「${playbookId}」匹配到 ${all.length} active playbook Agent(${all
83+
`\u73A9\u6CD5\u300C${playbookId}\u300D\u5339\u914D\u5230 ${all.length} \u4E2A active playbook Agent(${all
9184
.map((agent) => agent.id)
92-
.join(", ")});取最近更新的 ${winner.id}`,
85+
.join(", ")})\uFF1B\u53D6\u6700\u8FD1\u66F4\u65B0\u7684 ${winner.id}\u3002`,
9386
);
9487
},
9588
});
9689
}
97-
98-
type PlaybookSessionDetail = import("./sessions").PlaybookSessionDetail;

apps/server/src/services/sessions/playbook-session-adapter/provision.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { RemotePlaybookAgent } from "@openagentpack/playbooks";
21
import type { CloudAgent } from "@openagentpack/sdk";
32
import {
43
importResource,
@@ -9,6 +8,7 @@ import {
98
writeProjectRuntime,
109
} from "@openagentpack/sdk";
1110
import { loadAgentRuntimeInput } from "@/services/runtime-factory";
11+
import type { RemotePlaybookAgent } from "./runtime";
1212

1313
/**
1414
* Ensure a compiled catalog agent is provisioned to its provider (has a remote_id in state).

packages/playbooks/tests/session-runtime.test.ts renamed to apps/server/src/services/sessions/playbook-session-adapter/runtime.test.ts

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
import { describe, expect, test } from "bun:test";
2+
import { PLAYBOOK_APP_METADATA_KEY, PLAYBOOK_METADATA_KEY } from "@openagentpack/playbooks";
3+
import type { ProviderSessionEvent, Session } from "@openagentpack/sdk";
24
import {
35
createPlaybookSessionRuntime,
4-
PLAYBOOK_APP_METADATA_KEY,
5-
PLAYBOOK_METADATA_KEY,
66
PlaybookAgentIdentityMismatchError,
77
pickPlaybookAgent,
8-
playbookIdentityMismatchMessage,
98
type RemotePlaybookAgent,
109
readinessFromPick,
11-
} from "../src/index.ts";
10+
} from "./runtime";
11+
import type { PlaybookSessionDetail } from "./sessions";
1212

1313
const APP_ID = "agents-webui";
1414

1515
function agent(overrides: Partial<RemotePlaybookAgent> = {}): RemotePlaybookAgent {
1616
return {
1717
id: "agent_1",
18-
name: "Agents/设计师助手",
18+
name: "Agents/\u8BBE\u8BA1\u5E08\u52A9\u624B",
1919
metadata: {
2020
[PLAYBOOK_APP_METADATA_KEY]: APP_ID,
2121
[PLAYBOOK_METADATA_KEY]: "designer",
@@ -25,6 +25,22 @@ function agent(overrides: Partial<RemotePlaybookAgent> = {}): RemotePlaybookAgen
2525
};
2626
}
2727

28+
function fakeEvent(raw_type = "created"): ProviderSessionEvent {
29+
return { type: "status", raw_type, raw: {} };
30+
}
31+
32+
function fakeSession(id: string, agentId?: string): Session {
33+
return {
34+
session_id: id,
35+
status: "running",
36+
agent: agentId ? { agent_id: agentId } : undefined,
37+
};
38+
}
39+
40+
function fakeDetail(sessionId: string, agentId?: string): PlaybookSessionDetail {
41+
return { session: fakeSession(sessionId, agentId), events: [] };
42+
}
43+
2844
describe("pickPlaybookAgent", () => {
2945
test("picks the current-app playbook stamp by newest update time", () => {
3046
const older = agent({ id: "agent_old", updatedAt: "2026-06-20T00:00:00.000Z" });
@@ -45,7 +61,7 @@ describe("pickPlaybookAgent", () => {
4561
metadata: { [PLAYBOOK_APP_METADATA_KEY]: APP_ID },
4662
}),
4763
],
48-
{ playbookId: "designer", appId: APP_ID, expectedAgentName: "Agents/设计师助手" },
64+
{ playbookId: "designer", appId: APP_ID, expectedAgentName: "Agents/\u8BBE\u8BA1\u5E08\u52A9\u624B" },
4965
);
5066

5167
expect(pick.agent).toBeUndefined();
@@ -63,38 +79,23 @@ describe("pickPlaybookAgent", () => {
6379
});
6480

6581
describe("readinessFromPick", () => {
66-
test("a matched agent is ready and carries its remote id", () => {
67-
const pick = pickPlaybookAgent([agent({ id: "agent_ready" })], { playbookId: "designer", appId: APP_ID });
68-
69-
expect(readinessFromPick(pick, "designer")).toEqual({
82+
test("maps agent picks to playbook readiness states", () => {
83+
expect(readinessFromPick({ agent: agent(), duplicates: [], identityMismatch: false }, "designer")).toEqual({
7084
status: "ready",
7185
playbookId: "designer",
72-
remoteAgentId: "agent_ready",
86+
remoteAgentId: "agent_1",
7387
});
74-
});
75-
76-
test("no candidate is missing/not_provisioned", () => {
77-
const pick = pickPlaybookAgent([], { playbookId: "designer", appId: APP_ID });
7888

79-
expect(readinessFromPick(pick, "designer")).toEqual({
89+
expect(readinessFromPick({ agent: undefined, duplicates: [], identityMismatch: false }, "designer")).toEqual({
8090
status: "missing",
8191
playbookId: "designer",
8292
reason: "not_provisioned",
8393
});
84-
});
85-
86-
test("a same-name unstamped agent is blocked with the shared mismatch message", () => {
87-
const pick = pickPlaybookAgent([agent({ metadata: { [PLAYBOOK_APP_METADATA_KEY]: APP_ID } })], {
88-
playbookId: "designer",
89-
appId: APP_ID,
90-
expectedAgentName: "Agents/设计师助手",
91-
});
9294

93-
expect(readinessFromPick(pick, "designer")).toEqual({
95+
expect(readinessFromPick({ agent: undefined, duplicates: [], identityMismatch: true }, "designer")).toMatchObject({
9496
status: "blocked",
9597
playbookId: "designer",
9698
reason: "identity_mismatch",
97-
message: playbookIdentityMismatchMessage("designer"),
9899
});
99100
});
100101
});
@@ -103,10 +104,10 @@ describe("createPlaybookSessionRuntime", () => {
103104
test("start ensures the selected agent, starts the provider session, attaches events, then returns detail", async () => {
104105
const calls: string[] = [];
105106
const liveEvents = (async function* () {
106-
yield { type: "created" };
107+
yield fakeEvent();
107108
})();
108109
const runtime = createPlaybookSessionRuntime({
109-
identity: { appId: APP_ID, expectedAgentName: () => "Agents/设计师助手" },
110+
identity: { appId: APP_ID, expectedAgentName: () => "Agents/\u8BBE\u8BA1\u5E08\u52A9\u624B" },
110111
agents: {
111112
async listPlaybookAgents() {
112113
calls.push("agents.list");
@@ -133,7 +134,7 @@ describe("createPlaybookSessionRuntime", () => {
133134
},
134135
async getDetail(input) {
135136
calls.push(`sessions.detail:${input.sessionId}:${input.remoteAgentId}`);
136-
return { sessionId: input.sessionId, agentId: input.remoteAgentId };
137+
return fakeDetail(input.sessionId, input.remoteAgentId);
137138
},
138139
},
139140
events: {
@@ -153,7 +154,8 @@ describe("createPlaybookSessionRuntime", () => {
153154
model: "glm-5.1",
154155
});
155156

156-
expect(detail).toEqual({ sessionId: "sess_1", agentId: "agent_existing" });
157+
expect(detail.session.session_id).toBe("sess_1");
158+
expect(detail.session.agent?.agent_id).toBe("agent_existing");
157159
expect(calls).toEqual([
158160
"agents.list",
159161
"agents.ensure:agent_existing:glm-5.1",
@@ -165,7 +167,7 @@ describe("createPlaybookSessionRuntime", () => {
165167

166168
test("start throws before ensuring when identity is blocked", async () => {
167169
const runtime = createPlaybookSessionRuntime({
168-
identity: { appId: APP_ID, expectedAgentName: () => "Agents/设计师助手" },
170+
identity: { appId: APP_ID, expectedAgentName: () => "Agents/\u8BBE\u8BA1\u5E08\u52A9\u624B" },
169171
agents: {
170172
async listPlaybookAgents() {
171173
return [agent({ metadata: { [PLAYBOOK_APP_METADATA_KEY]: APP_ID } })];
@@ -201,7 +203,7 @@ describe("createPlaybookSessionRuntime", () => {
201203
test("send appends a message, attaches events, then returns detail", async () => {
202204
const calls: string[] = [];
203205
const liveEvents = (async function* () {
204-
yield { type: "message" };
206+
yield fakeEvent("message");
205207
})();
206208
const runtime = createPlaybookSessionRuntime({
207209
identity: { appId: APP_ID },
@@ -229,7 +231,7 @@ describe("createPlaybookSessionRuntime", () => {
229231
},
230232
async getDetail(input) {
231233
calls.push(`sessions.detail:${input.sessionId}:${input.playbookId}`);
232-
return { sessionId: input.sessionId, playbookId: input.playbookId };
234+
return fakeDetail(input.sessionId);
233235
},
234236
},
235237
events: {
@@ -244,7 +246,7 @@ describe("createPlaybookSessionRuntime", () => {
244246

245247
const detail = await runtime.send({ playbookId: "designer", sessionId: "sess_1", message: "continue" });
246248

247-
expect(detail).toEqual({ sessionId: "sess_1", playbookId: "designer" });
249+
expect(detail.session.session_id).toBe("sess_1");
248250
expect(calls).toEqual([
249251
"sessions.send:sess_1:designer:continue",
250252
"events.attach:sess_1",
@@ -267,7 +269,7 @@ describe("createPlaybookSessionRuntime", () => {
267269
sessions: {
268270
async list(input) {
269271
calls.push(`sessions.list:${input.playbookId}:${input.remoteAgentId}:${input.limit}:${input.pageToken}`);
270-
return { sessions: [{ id: "sess_1" }], nextPageToken: "next" };
272+
return { sessions: [fakeSession("sess_1")], nextPageToken: "next" };
271273
},
272274
async start() {
273275
throw new Error("should not start");
@@ -291,7 +293,8 @@ describe("createPlaybookSessionRuntime", () => {
291293
pageToken: "page_1",
292294
});
293295

294-
expect(listed).toEqual({ sessions: [{ id: "sess_1" }], nextPageToken: "next" });
296+
expect(listed.sessions[0]?.session_id).toBe("sess_1");
297+
expect(listed.nextPageToken).toBe("next");
295298
expect(calls).toEqual(["sessions.list:designer:agent_1:10:page_1"]);
296299
});
297300

@@ -322,14 +325,14 @@ describe("createPlaybookSessionRuntime", () => {
322325
},
323326
async getDetail(input) {
324327
calls.push(`sessions.detail:${input.sessionId}:${input.playbookId}`);
325-
return { sessionId: input.sessionId, playbookId: input.playbookId };
328+
return fakeDetail(input.sessionId);
326329
},
327330
},
328331
});
329332

330333
const detail = await runtime.getDetail({ playbookId: "designer", sessionId: "sess_1" });
331334

332-
expect(detail).toEqual({ sessionId: "sess_1", playbookId: "designer" });
335+
expect(detail.session.session_id).toBe("sess_1");
333336
expect(calls).toEqual(["sessions.detail:sess_1:designer"]);
334337
});
335338

0 commit comments

Comments
 (0)