@mawaru/sdk 0.38.0 → 0.39.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.
- package/_schemas/ai-manifest.test.ts +8 -0
- package/_schemas/ai-manifest.ts +2 -0
- package/_schemas/graph.test.ts +36 -0
- package/_schemas/graph.ts +3 -2
- package/_schemas/node.ts +4 -0
- package/_schemas/port-spec.test.ts +124 -0
- package/_schemas/port-spec.ts +67 -2
- package/dist/_schemas/ai-manifest.d.ts +3 -0
- package/dist/_schemas/ai-manifest.js +2 -0
- package/dist/_schemas/graph.d.ts +2 -0
- package/dist/_schemas/graph.js +3 -2
- package/dist/_schemas/node.d.ts +1 -0
- package/dist/_schemas/node.js +4 -0
- package/dist/_schemas/port-spec.d.ts +2 -0
- package/dist/_schemas/port-spec.js +57 -2
- package/docs/development.md +5 -3
- package/package.json +1 -1
|
@@ -145,6 +145,14 @@ describe("aiManifestSchema", () => {
|
|
|
145
145
|
expect(parsed.inputs.instruction?.resume).toBe(true)
|
|
146
146
|
})
|
|
147
147
|
|
|
148
|
+
it("inputs の flexible(推奨型を維持して任意の型を受ける)が読める", () => {
|
|
149
|
+
const parsed = aiManifestSchema.parse({
|
|
150
|
+
...valid,
|
|
151
|
+
inputs: { in: { schema: { type: "object" }, flexible: true } },
|
|
152
|
+
})
|
|
153
|
+
expect(parsed.inputs.in?.flexible).toBe(true)
|
|
154
|
+
})
|
|
155
|
+
|
|
148
156
|
it("旧形式(値が素の JSON Schema)は { schema: 値 } として読める", () => {
|
|
149
157
|
const legacy = aiManifestSchema.parse({
|
|
150
158
|
...valid,
|
package/_schemas/ai-manifest.ts
CHANGED
|
@@ -72,6 +72,8 @@ export const agentOfModel = (model: AiModel): AiAgentId =>
|
|
|
72
72
|
export const aiInputDeclSchema = z.object({
|
|
73
73
|
schema: jsonSchemaSchema,
|
|
74
74
|
resume: z.boolean().optional(),
|
|
75
|
+
// 推奨型を維持して任意の型を受ける(接続で上書きされず、適合チェックもしない)
|
|
76
|
+
flexible: z.boolean().optional(),
|
|
75
77
|
})
|
|
76
78
|
export type AiInputDecl = z.infer<typeof aiInputDeclSchema>
|
|
77
79
|
|
package/_schemas/graph.test.ts
CHANGED
|
@@ -1203,6 +1203,42 @@ describe("saveGraphBodySchema:コンポーネントノード(component kind
|
|
|
1203
1203
|
).not.toThrow()
|
|
1204
1204
|
})
|
|
1205
1205
|
|
|
1206
|
+
it("flexible な in(契約で焼き込まれた component.in・AI の宣言 in)への接続は型が違っても保存できる", () => {
|
|
1207
|
+
const prog = {
|
|
1208
|
+
...makeNode([
|
|
1209
|
+
port("in", "in"),
|
|
1210
|
+
{ ...port("out", "main"), schema: { type: "string" } },
|
|
1211
|
+
]),
|
|
1212
|
+
kind: "program" as const,
|
|
1213
|
+
}
|
|
1214
|
+
const comp = {
|
|
1215
|
+
...componentNode({ type: "number" }),
|
|
1216
|
+
}
|
|
1217
|
+
comp.ports = comp.ports.map((p) =>
|
|
1218
|
+
p.io === "in" ? { ...p, flexible: true } : p,
|
|
1219
|
+
)
|
|
1220
|
+
expect(() =>
|
|
1221
|
+
saveGraphBodySchema.parse({
|
|
1222
|
+
nodes: [prog, comp],
|
|
1223
|
+
connections: [connect(prog, comp)],
|
|
1224
|
+
}),
|
|
1225
|
+
).not.toThrow()
|
|
1226
|
+
// AI の flexible な in:鏡映されず推奨型 {type:"number"} が残るが、型が違う接続を通す
|
|
1227
|
+
const ai = {
|
|
1228
|
+
...makeNode([
|
|
1229
|
+
{ ...port("in", "in"), schema: { type: "number" }, flexible: true },
|
|
1230
|
+
port("out", "main"),
|
|
1231
|
+
]),
|
|
1232
|
+
kind: "ai" as const,
|
|
1233
|
+
}
|
|
1234
|
+
expect(() =>
|
|
1235
|
+
saveGraphBodySchema.parse({
|
|
1236
|
+
nodes: [prog, ai],
|
|
1237
|
+
connections: [connect(prog, ai)],
|
|
1238
|
+
}),
|
|
1239
|
+
).not.toThrow()
|
|
1240
|
+
})
|
|
1241
|
+
|
|
1206
1242
|
it("component の in への接続は契約への適合を検証する(伝播型 ⊆ 契約)", () => {
|
|
1207
1243
|
// 上流 program の out {type:"string"} → component の in {type:"number"} は不適合
|
|
1208
1244
|
const prog = {
|
package/_schemas/graph.ts
CHANGED
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
portKeySchema,
|
|
7
7
|
} from "./node.js"
|
|
8
8
|
import {
|
|
9
|
+
connectionAccepted,
|
|
9
10
|
hasInboundConflict,
|
|
10
|
-
isSchemaSubset,
|
|
11
11
|
kindPortsViolation,
|
|
12
12
|
resolvePortSchemas,
|
|
13
13
|
UNDECLARED_SCHEMA,
|
|
@@ -551,9 +551,10 @@ export const saveGraphBodySchema = z
|
|
|
551
551
|
// 接続鏡映する口(program / ai の in)とコピー宣言のビューは接続元の型を採用する
|
|
552
552
|
// ので自明に通り、実質の検証対象は宣言で固定された口
|
|
553
553
|
// (component の契約・宣言のあるビューの human.in)になる
|
|
554
|
+
// flexible な in(AI の宣言 in・契約で焼き込まれた component の in)は型を問わない
|
|
554
555
|
const fromSchema = resolved.get(conn.from_port_id) ?? from.schema
|
|
555
556
|
const toSchema = resolved.get(conn.to_port_id) ?? to.schema
|
|
556
|
-
if (!
|
|
557
|
+
if (!connectionAccepted(fromSchema, { ...to, schema: toSchema })) {
|
|
557
558
|
ctx.addIssue({
|
|
558
559
|
code: "custom",
|
|
559
560
|
path: ["connections", i, "to_port_id"],
|
package/_schemas/node.ts
CHANGED
|
@@ -54,6 +54,10 @@ export const graphPortSchema = z
|
|
|
54
54
|
// 再開する(AI ノードで意味を持つ。key ではなくこの属性が再開を決める。
|
|
55
55
|
// ポート型固定の撤去とセッション再開のポート属性化.md)
|
|
56
56
|
resume: z.boolean().optional(),
|
|
57
|
+
// in ポートの型属性:true なら宣言(推奨型)を維持し、接続元の型による上書き・
|
|
58
|
+
// 適合チェックをしない(AI の in で設定。start / component へは導出で流れる。
|
|
59
|
+
// AIノード入力のflexible属性(任意の型で呼べるループ).md)
|
|
60
|
+
flexible: z.boolean().optional(),
|
|
57
61
|
side: portSideSchema.optional(),
|
|
58
62
|
offset: z.number().min(0).max(1).optional(),
|
|
59
63
|
})
|
|
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"
|
|
|
2
2
|
import type { GraphPort, JsonSchema } from "./node.js"
|
|
3
3
|
import {
|
|
4
4
|
arrayOf,
|
|
5
|
+
connectionAccepted,
|
|
5
6
|
dataFormatOf,
|
|
6
7
|
dataSchemaOf,
|
|
7
8
|
fileSchema,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
kindPortsViolation,
|
|
13
14
|
mergeInboundSchemas,
|
|
14
15
|
propagateDerivedPorts,
|
|
16
|
+
resolveFlexiblePorts,
|
|
15
17
|
resolvePortSchemas,
|
|
16
18
|
schemaFit,
|
|
17
19
|
schemaTypeKey,
|
|
@@ -661,6 +663,128 @@ describe("型伝播(導出ポートの解決)", () => {
|
|
|
661
663
|
expect(resolved.get(start.outMain.id)).toEqual(UNDECLARED_SCHEMA)
|
|
662
664
|
})
|
|
663
665
|
|
|
666
|
+
// flexible:AI の in の型属性。宣言(推奨型)を維持し、接続の型を問わない。
|
|
667
|
+
// start へは型と同じ経路で導出される(AIノード入力のflexible属性(任意の型で呼べるループ).md)
|
|
668
|
+
describe("flexible(推奨型を維持して任意の型を受ける in)", () => {
|
|
669
|
+
const flexibleAi = (declared: JsonSchema) => {
|
|
670
|
+
const inPort: GraphPort = {
|
|
671
|
+
...port("in", "in", declared),
|
|
672
|
+
flexible: true,
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
node: { id: uuid(), kind: "ai", ports: [inPort, port("out", "main")] },
|
|
676
|
+
inPort,
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const recommended = {
|
|
680
|
+
type: "object",
|
|
681
|
+
properties: { prompt: { type: "string" } },
|
|
682
|
+
required: ["prompt"],
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
it("flexible な in は接続元の具体型で上書きしない(宣言=推奨型を維持)", () => {
|
|
686
|
+
const program = programNode({ type: "string" })
|
|
687
|
+
const progOut = program.node.ports.find(
|
|
688
|
+
(p) => p.io === "out",
|
|
689
|
+
) as GraphPort
|
|
690
|
+
progOut.schema = declaredOut
|
|
691
|
+
const ai = flexibleAi(recommended)
|
|
692
|
+
const resolved = resolvePortSchemas(
|
|
693
|
+
[program.node, ai.node],
|
|
694
|
+
[{ from_port_id: progOut.id, to_port_id: ai.inPort.id }],
|
|
695
|
+
)
|
|
696
|
+
expect(resolved.get(ai.inPort.id)).toEqual(recommended)
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
it("flexible な in は型の割れた fan-in でも推奨型を維持する", () => {
|
|
700
|
+
const a = programNode({ type: "string" })
|
|
701
|
+
const aOut = a.node.ports.find((p) => p.io === "out") as GraphPort
|
|
702
|
+
aOut.schema = { type: "string" }
|
|
703
|
+
const b = programNode({ type: "string" })
|
|
704
|
+
const bOut = b.node.ports.find((p) => p.io === "out") as GraphPort
|
|
705
|
+
bOut.schema = { type: "number" }
|
|
706
|
+
const ai = flexibleAi(recommended)
|
|
707
|
+
const resolved = resolvePortSchemas(
|
|
708
|
+
[a.node, b.node, ai.node],
|
|
709
|
+
[
|
|
710
|
+
{ from_port_id: aOut.id, to_port_id: ai.inPort.id },
|
|
711
|
+
{ from_port_id: bOut.id, to_port_id: ai.inPort.id },
|
|
712
|
+
],
|
|
713
|
+
)
|
|
714
|
+
expect(resolved.get(ai.inPort.id)).toEqual(recommended)
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
it("start → AI(flexible) 直結:start の in / out main が推奨型で flexible になる", () => {
|
|
718
|
+
const start = startNode()
|
|
719
|
+
const ai = flexibleAi(recommended)
|
|
720
|
+
const connections = [
|
|
721
|
+
{ from_port_id: start.outMain.id, to_port_id: ai.inPort.id },
|
|
722
|
+
]
|
|
723
|
+
expect(resolveFlexiblePorts([start.node, ai.node], connections)).toEqual(
|
|
724
|
+
new Set([start.inPort.id, start.outMain.id]),
|
|
725
|
+
)
|
|
726
|
+
const propagated = propagateDerivedPorts(
|
|
727
|
+
[start.node, ai.node],
|
|
728
|
+
connections,
|
|
729
|
+
)[0]
|
|
730
|
+
if (!propagated) throw new Error("start not propagated")
|
|
731
|
+
for (const p of propagated.ports) {
|
|
732
|
+
expect(p.schema).toEqual(recommended)
|
|
733
|
+
expect(p.flexible).toBe(true)
|
|
734
|
+
}
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
it("start → [AI(flexible), program]:非 flexible の接続先が混ざれば start は flexible にならない", () => {
|
|
738
|
+
const start = startNode()
|
|
739
|
+
const ai = flexibleAi(recommended)
|
|
740
|
+
const program = programNode(recommended)
|
|
741
|
+
const connections = [
|
|
742
|
+
{ from_port_id: start.outMain.id, to_port_id: ai.inPort.id },
|
|
743
|
+
{ from_port_id: start.outMain.id, to_port_id: program.inPort.id },
|
|
744
|
+
]
|
|
745
|
+
expect(
|
|
746
|
+
resolveFlexiblePorts([start.node, ai.node, program.node], connections),
|
|
747
|
+
).toEqual(new Set())
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
it("start:未接続なら flexible にならず、ports に残った古い flexible は落とす", () => {
|
|
751
|
+
const inPort: GraphPort = { ...port("in", "in"), flexible: true }
|
|
752
|
+
const outMain: GraphPort = { ...port("out", "main"), flexible: true }
|
|
753
|
+
const start = { id: uuid(), kind: "start", ports: [inPort, outMain] }
|
|
754
|
+
const propagated = propagateDerivedPorts([start], [])[0]
|
|
755
|
+
if (!propagated) throw new Error("start not propagated")
|
|
756
|
+
for (const p of propagated.ports) expect(p.flexible).toBeUndefined()
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
it("component の in(契約の焼き込みで flexible)に直結した start も flexible(推移)", () => {
|
|
760
|
+
const start = startNode()
|
|
761
|
+
const compIn: GraphPort = {
|
|
762
|
+
...port("in", "in", recommended),
|
|
763
|
+
flexible: true,
|
|
764
|
+
}
|
|
765
|
+
const component = {
|
|
766
|
+
id: uuid(),
|
|
767
|
+
kind: "component",
|
|
768
|
+
ports: [compIn, port("out", "main")],
|
|
769
|
+
}
|
|
770
|
+
expect(
|
|
771
|
+
resolveFlexiblePorts(
|
|
772
|
+
[start.node, component],
|
|
773
|
+
[{ from_port_id: start.outMain.id, to_port_id: compIn.id }],
|
|
774
|
+
),
|
|
775
|
+
).toEqual(new Set([start.inPort.id, start.outMain.id]))
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
it("connectionAccepted:受け側が flexible なら型を問わず可、そうでなければ isSchemaSubset", () => {
|
|
779
|
+
const to = port("in", "in", recommended)
|
|
780
|
+
expect(connectionAccepted({ type: "string" }, to)).toBe(false)
|
|
781
|
+
expect(
|
|
782
|
+
connectionAccepted({ type: "string" }, { ...to, flexible: true }),
|
|
783
|
+
).toBe(true)
|
|
784
|
+
expect(connectionAccepted(recommended, to)).toBe(true)
|
|
785
|
+
})
|
|
786
|
+
})
|
|
787
|
+
|
|
664
788
|
it("propagateDerivedPorts:schema を書き戻し、変化のないノードは同じ参照を返す", () => {
|
|
665
789
|
const progOut = port("out", "main", declaredOut)
|
|
666
790
|
const program = {
|
package/_schemas/port-spec.ts
CHANGED
|
@@ -487,6 +487,8 @@ export const resolvePortSchemas = <N extends PropagationNode>(
|
|
|
487
487
|
if (isMirror) {
|
|
488
488
|
for (const port of node.ports) {
|
|
489
489
|
if (port.io !== "in") continue
|
|
490
|
+
// flexible な in は宣言(推奨型)を維持する(接続の型を問わない口)
|
|
491
|
+
if (port.flexible === true) continue
|
|
490
492
|
const sources = inbound.get(port.id) ?? []
|
|
491
493
|
const inSchema = mergeInboundSchemas(
|
|
492
494
|
sources.map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA),
|
|
@@ -535,19 +537,82 @@ export const resolvePortSchemas = <N extends PropagationNode>(
|
|
|
535
537
|
|
|
536
538
|
// ノード配列へ伝播結果を書き戻す(接続変更時のポートデータ自動更新)。
|
|
537
539
|
// schema が変わらないポート・ノードは同じ参照を返す
|
|
540
|
+
// flexible の導出:start の in / out main は、接続先の in が1つ以上あり**全て** flexible
|
|
541
|
+
// (AI の宣言 in・契約で焼き込まれた component の in)のときだけ flexible。
|
|
542
|
+
// 非 flexible の接続先(program 等)が1つでも混ざれば型どおりの入力が要るので false。
|
|
543
|
+
// 直結のみ(wait 等の素通し経由は型と同じく流さない)。戻りは flexible になる port_id の集合
|
|
544
|
+
export const resolveFlexiblePorts = (
|
|
545
|
+
nodes: PropagationNode[],
|
|
546
|
+
connections: PortLink[],
|
|
547
|
+
): Set<string> => {
|
|
548
|
+
const flexibleByPortId = new Map<string, boolean>()
|
|
549
|
+
for (const node of nodes) {
|
|
550
|
+
if (node.kind === "start") continue
|
|
551
|
+
for (const port of node.ports) {
|
|
552
|
+
if (port.io === "in")
|
|
553
|
+
flexibleByPortId.set(port.id, port.flexible === true)
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const outbound = new Map<string, string[]>()
|
|
557
|
+
for (const conn of connections) {
|
|
558
|
+
const outs = outbound.get(conn.from_port_id) ?? []
|
|
559
|
+
outs.push(conn.to_port_id)
|
|
560
|
+
outbound.set(conn.from_port_id, outs)
|
|
561
|
+
}
|
|
562
|
+
const flexible = new Set<string>()
|
|
563
|
+
for (const node of nodes) {
|
|
564
|
+
if (node.kind !== "start") continue
|
|
565
|
+
const inPort = node.ports.find((p) => p.io === "in")
|
|
566
|
+
const outMain = node.ports.find((p) => p.io === "out" && p.key === "main")
|
|
567
|
+
if (!inPort || !outMain) continue
|
|
568
|
+
const targets = outbound.get(outMain.id) ?? []
|
|
569
|
+
if (
|
|
570
|
+
targets.length > 0 &&
|
|
571
|
+
targets.every((id) => flexibleByPortId.get(id) === true)
|
|
572
|
+
) {
|
|
573
|
+
flexible.add(inPort.id)
|
|
574
|
+
flexible.add(outMain.id)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return flexible
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// 接続の型が受け入れられるか:受け側が flexible なら常に可、それ以外は 伝播型 ⊆ 宣言。
|
|
581
|
+
// sdk の保存契約・backend の保存時チェック・エディタの表示はすべてこれを使う
|
|
582
|
+
// (判定が散ると表示と保存がズレる)
|
|
583
|
+
export const connectionAccepted = (
|
|
584
|
+
from: JsonSchema,
|
|
585
|
+
to: Pick<GraphPort, "schema" | "flexible">,
|
|
586
|
+
): boolean => to.flexible === true || isSchemaSubset(from, to.schema)
|
|
587
|
+
|
|
538
588
|
export const propagateDerivedPorts = <N extends PropagationNode>(
|
|
539
589
|
nodes: N[],
|
|
540
590
|
connections: PortLink[],
|
|
541
591
|
options?: PropagationOptions<N>,
|
|
542
592
|
): N[] => {
|
|
543
593
|
const resolved = resolvePortSchemas(nodes, connections, options)
|
|
594
|
+
const flexiblePorts = resolveFlexiblePorts(nodes, connections)
|
|
544
595
|
return nodes.map((node) => {
|
|
545
596
|
let touched = false
|
|
546
597
|
const ports = node.ports.map((port) => {
|
|
547
598
|
const schema = resolved.get(port.id) ?? port.schema
|
|
548
|
-
|
|
599
|
+
// start の flexible は導出結果で置き換える(false は省略=キー無し)
|
|
600
|
+
const flexible =
|
|
601
|
+
node.kind === "start"
|
|
602
|
+
? flexiblePorts.has(port.id) || undefined
|
|
603
|
+
: port.flexible
|
|
604
|
+
if (
|
|
605
|
+
JSON.stringify(schema) === JSON.stringify(port.schema) &&
|
|
606
|
+
flexible === port.flexible
|
|
607
|
+
)
|
|
608
|
+
return port
|
|
549
609
|
touched = true
|
|
550
|
-
|
|
610
|
+
const { flexible: _dropped, ...rest } = port
|
|
611
|
+
return {
|
|
612
|
+
...rest,
|
|
613
|
+
schema,
|
|
614
|
+
...(flexible ? { flexible } : {}),
|
|
615
|
+
} as typeof port
|
|
551
616
|
})
|
|
552
617
|
return touched ? { ...node, ports } : node
|
|
553
618
|
})
|
|
@@ -49,6 +49,7 @@ export declare const agentOfModel: (model: AiModel) => AiAgentId;
|
|
|
49
49
|
export declare const aiInputDeclSchema: z.ZodObject<{
|
|
50
50
|
schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
51
51
|
resume: z.ZodOptional<z.ZodBoolean>;
|
|
52
|
+
flexible: z.ZodOptional<z.ZodBoolean>;
|
|
52
53
|
}, z.core.$strip>;
|
|
53
54
|
export type AiInputDecl = z.infer<typeof aiInputDeclSchema>;
|
|
54
55
|
export declare const aiManifestSchema: z.ZodObject<{
|
|
@@ -87,9 +88,11 @@ export declare const aiManifestSchema: z.ZodObject<{
|
|
|
87
88
|
inputs: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
|
|
88
89
|
schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
89
90
|
resume: z.ZodOptional<z.ZodBoolean>;
|
|
91
|
+
flexible: z.ZodOptional<z.ZodBoolean>;
|
|
90
92
|
}, z.core.$strip>, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodTransform<{
|
|
91
93
|
schema: Record<string, unknown>;
|
|
92
94
|
resume?: boolean | undefined;
|
|
95
|
+
flexible?: boolean | undefined;
|
|
93
96
|
}, Record<string, unknown>>>]>>;
|
|
94
97
|
outputs: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
95
98
|
}, z.core.$strip>;
|
|
@@ -65,6 +65,8 @@ export const agentOfModel = (model) => model.startsWith("claude-") ? "claude" :
|
|
|
65
65
|
export const aiInputDeclSchema = z.object({
|
|
66
66
|
schema: jsonSchemaSchema,
|
|
67
67
|
resume: z.boolean().optional(),
|
|
68
|
+
// 推奨型を維持して任意の型を受ける(接続で上書きされず、適合チェックもしない)
|
|
69
|
+
flexible: z.boolean().optional(),
|
|
68
70
|
});
|
|
69
71
|
// 旧形式(値が素の JSON Schema)は { schema: 値 } として読む。判別は「schema キーを
|
|
70
72
|
// 持つか」:JSON Schema のトップレベルに "schema" キーは現れない(メタスキーマ参照は
|
package/dist/_schemas/graph.d.ts
CHANGED
|
@@ -152,6 +152,7 @@ export declare const graphNodeSchema: z.ZodObject<{
|
|
|
152
152
|
name: z.ZodString;
|
|
153
153
|
schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
154
154
|
resume: z.ZodOptional<z.ZodBoolean>;
|
|
155
|
+
flexible: z.ZodOptional<z.ZodBoolean>;
|
|
155
156
|
side: z.ZodOptional<z.ZodEnum<{
|
|
156
157
|
top: "top";
|
|
157
158
|
right: "right";
|
|
@@ -252,6 +253,7 @@ export declare const saveGraphBodySchema: z.ZodObject<{
|
|
|
252
253
|
name: z.ZodString;
|
|
253
254
|
schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
254
255
|
resume: z.ZodOptional<z.ZodBoolean>;
|
|
256
|
+
flexible: z.ZodOptional<z.ZodBoolean>;
|
|
255
257
|
side: z.ZodOptional<z.ZodEnum<{
|
|
256
258
|
top: "top";
|
|
257
259
|
right: "right";
|
package/dist/_schemas/graph.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { graphPortSchema, jsonSchemaSchema, nodeKindSchema, portKeySchema, } from "./node.js";
|
|
3
|
-
import {
|
|
3
|
+
import { connectionAccepted, hasInboundConflict, kindPortsViolation, resolvePortSchemas, UNDECLARED_SCHEMA, } from "./port-spec.js";
|
|
4
4
|
// refs=ループ変数の袋(key→任意の JSON 値。上流出力の参照(refs).md)。
|
|
5
5
|
// run 開始時に定義から複製され、全 step に全量差し込まれる。
|
|
6
6
|
// key はポート key と同じ規則(^[a-z][a-z0-9_]*$)。実行系ボディ
|
|
@@ -473,9 +473,10 @@ export const saveGraphBodySchema = z
|
|
|
473
473
|
// 接続鏡映する口(program / ai の in)とコピー宣言のビューは接続元の型を採用する
|
|
474
474
|
// ので自明に通り、実質の検証対象は宣言で固定された口
|
|
475
475
|
// (component の契約・宣言のあるビューの human.in)になる
|
|
476
|
+
// flexible な in(AI の宣言 in・契約で焼き込まれた component の in)は型を問わない
|
|
476
477
|
const fromSchema = resolved.get(conn.from_port_id) ?? from.schema;
|
|
477
478
|
const toSchema = resolved.get(conn.to_port_id) ?? to.schema;
|
|
478
|
-
if (!
|
|
479
|
+
if (!connectionAccepted(fromSchema, { ...to, schema: toSchema })) {
|
|
479
480
|
ctx.addIssue({
|
|
480
481
|
code: "custom",
|
|
481
482
|
path: ["connections", i, "to_port_id"],
|
package/dist/_schemas/node.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ export declare const graphPortSchema: z.ZodObject<{
|
|
|
39
39
|
name: z.ZodString;
|
|
40
40
|
schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
41
41
|
resume: z.ZodOptional<z.ZodBoolean>;
|
|
42
|
+
flexible: z.ZodOptional<z.ZodBoolean>;
|
|
42
43
|
side: z.ZodOptional<z.ZodEnum<{
|
|
43
44
|
top: "top";
|
|
44
45
|
right: "right";
|
package/dist/_schemas/node.js
CHANGED
|
@@ -43,6 +43,10 @@ export const graphPortSchema = z
|
|
|
43
43
|
// 再開する(AI ノードで意味を持つ。key ではなくこの属性が再開を決める。
|
|
44
44
|
// ポート型固定の撤去とセッション再開のポート属性化.md)
|
|
45
45
|
resume: z.boolean().optional(),
|
|
46
|
+
// in ポートの型属性:true なら宣言(推奨型)を維持し、接続元の型による上書き・
|
|
47
|
+
// 適合チェックをしない(AI の in で設定。start / component へは導出で流れる。
|
|
48
|
+
// AIノード入力のflexible属性(任意の型で呼べるループ).md)
|
|
49
|
+
flexible: z.boolean().optional(),
|
|
46
50
|
side: portSideSchema.optional(),
|
|
47
51
|
offset: z.number().min(0).max(1).optional(),
|
|
48
52
|
})
|
|
@@ -70,6 +70,8 @@ export type PropagationOptions<N> = {
|
|
|
70
70
|
} | null | undefined;
|
|
71
71
|
};
|
|
72
72
|
export declare const resolvePortSchemas: <N extends PropagationNode>(nodes: N[], connections: PortLink[], options?: PropagationOptions<N>) => Map<string, JsonSchema>;
|
|
73
|
+
export declare const resolveFlexiblePorts: (nodes: PropagationNode[], connections: PortLink[]) => Set<string>;
|
|
74
|
+
export declare const connectionAccepted: (from: JsonSchema, to: Pick<GraphPort, "schema" | "flexible">) => boolean;
|
|
73
75
|
export declare const propagateDerivedPorts: <N extends PropagationNode>(nodes: N[], connections: PortLink[], options?: PropagationOptions<N>) => N[];
|
|
74
76
|
export type SchemaFit = "ok" | "mismatch" | "unknown";
|
|
75
77
|
export declare const schemaFit: (sub: JsonSchema, sup: JsonSchema) => SchemaFit;
|
|
@@ -382,6 +382,9 @@ export const resolvePortSchemas = (nodes, connections, options) => {
|
|
|
382
382
|
for (const port of node.ports) {
|
|
383
383
|
if (port.io !== "in")
|
|
384
384
|
continue;
|
|
385
|
+
// flexible な in は宣言(推奨型)を維持する(接続の型を問わない口)
|
|
386
|
+
if (port.flexible === true)
|
|
387
|
+
continue;
|
|
385
388
|
const sources = inbound.get(port.id) ?? [];
|
|
386
389
|
const inSchema = mergeInboundSchemas(sources.map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA));
|
|
387
390
|
if (!isUndeclaredSchema(inSchema))
|
|
@@ -426,16 +429,68 @@ export const resolvePortSchemas = (nodes, connections, options) => {
|
|
|
426
429
|
};
|
|
427
430
|
// ノード配列へ伝播結果を書き戻す(接続変更時のポートデータ自動更新)。
|
|
428
431
|
// schema が変わらないポート・ノードは同じ参照を返す
|
|
432
|
+
// flexible の導出:start の in / out main は、接続先の in が1つ以上あり**全て** flexible
|
|
433
|
+
// (AI の宣言 in・契約で焼き込まれた component の in)のときだけ flexible。
|
|
434
|
+
// 非 flexible の接続先(program 等)が1つでも混ざれば型どおりの入力が要るので false。
|
|
435
|
+
// 直結のみ(wait 等の素通し経由は型と同じく流さない)。戻りは flexible になる port_id の集合
|
|
436
|
+
export const resolveFlexiblePorts = (nodes, connections) => {
|
|
437
|
+
const flexibleByPortId = new Map();
|
|
438
|
+
for (const node of nodes) {
|
|
439
|
+
if (node.kind === "start")
|
|
440
|
+
continue;
|
|
441
|
+
for (const port of node.ports) {
|
|
442
|
+
if (port.io === "in")
|
|
443
|
+
flexibleByPortId.set(port.id, port.flexible === true);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const outbound = new Map();
|
|
447
|
+
for (const conn of connections) {
|
|
448
|
+
const outs = outbound.get(conn.from_port_id) ?? [];
|
|
449
|
+
outs.push(conn.to_port_id);
|
|
450
|
+
outbound.set(conn.from_port_id, outs);
|
|
451
|
+
}
|
|
452
|
+
const flexible = new Set();
|
|
453
|
+
for (const node of nodes) {
|
|
454
|
+
if (node.kind !== "start")
|
|
455
|
+
continue;
|
|
456
|
+
const inPort = node.ports.find((p) => p.io === "in");
|
|
457
|
+
const outMain = node.ports.find((p) => p.io === "out" && p.key === "main");
|
|
458
|
+
if (!inPort || !outMain)
|
|
459
|
+
continue;
|
|
460
|
+
const targets = outbound.get(outMain.id) ?? [];
|
|
461
|
+
if (targets.length > 0 &&
|
|
462
|
+
targets.every((id) => flexibleByPortId.get(id) === true)) {
|
|
463
|
+
flexible.add(inPort.id);
|
|
464
|
+
flexible.add(outMain.id);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return flexible;
|
|
468
|
+
};
|
|
469
|
+
// 接続の型が受け入れられるか:受け側が flexible なら常に可、それ以外は 伝播型 ⊆ 宣言。
|
|
470
|
+
// sdk の保存契約・backend の保存時チェック・エディタの表示はすべてこれを使う
|
|
471
|
+
// (判定が散ると表示と保存がズレる)
|
|
472
|
+
export const connectionAccepted = (from, to) => to.flexible === true || isSchemaSubset(from, to.schema);
|
|
429
473
|
export const propagateDerivedPorts = (nodes, connections, options) => {
|
|
430
474
|
const resolved = resolvePortSchemas(nodes, connections, options);
|
|
475
|
+
const flexiblePorts = resolveFlexiblePorts(nodes, connections);
|
|
431
476
|
return nodes.map((node) => {
|
|
432
477
|
let touched = false;
|
|
433
478
|
const ports = node.ports.map((port) => {
|
|
434
479
|
const schema = resolved.get(port.id) ?? port.schema;
|
|
435
|
-
|
|
480
|
+
// start の flexible は導出結果で置き換える(false は省略=キー無し)
|
|
481
|
+
const flexible = node.kind === "start"
|
|
482
|
+
? flexiblePorts.has(port.id) || undefined
|
|
483
|
+
: port.flexible;
|
|
484
|
+
if (JSON.stringify(schema) === JSON.stringify(port.schema) &&
|
|
485
|
+
flexible === port.flexible)
|
|
436
486
|
return port;
|
|
437
487
|
touched = true;
|
|
438
|
-
|
|
488
|
+
const { flexible: _dropped, ...rest } = port;
|
|
489
|
+
return {
|
|
490
|
+
...rest,
|
|
491
|
+
schema,
|
|
492
|
+
...(flexible ? { flexible } : {}),
|
|
493
|
+
};
|
|
439
494
|
});
|
|
440
495
|
return touched ? { ...node, ports } : node;
|
|
441
496
|
});
|
package/docs/development.md
CHANGED
|
@@ -56,10 +56,12 @@ views/
|
|
|
56
56
|
走り続ける)。省略時は `claude-opus`。認証は repo の Actions secrets に置く:Claude Code は
|
|
57
57
|
`ANTHROPIC_API_KEY`(または `CLAUDE_CODE_OAUTH_TOKEN`)、Codex は明示的な `auth_mode` と対応する認証情報(下記「Codex の認証方式」参照)
|
|
58
58
|
(`env` に宣言しなくても渡される)
|
|
59
|
-
- `inputs` は1件以上・key 自由。値は `{ "schema": <JSON Schema>, "resume": true }` の形
|
|
59
|
+
- `inputs` は1件以上・key 自由。値は `{ "schema": <JSON Schema>, "resume": true, "flexible": true }` の形
|
|
60
60
|
(`resume` は省略可。true の口から入力が届いた実行は、同じノードの直近セッションを
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
復元して再開する——差し戻し→修正の続き実行に使う。`flexible` も省略可。true の口は
|
|
62
|
+
`schema` を推奨型として維持し、接続で型が上書きされず型の違う接続も通る——この AI が
|
|
63
|
+
先頭のループを他のループや API から型を揃えずに呼べるようにする)。旧形式(値が素の
|
|
64
|
+
JSON Schema)も読める
|
|
63
65
|
- 実行時はラッパーがエージェント CLI(Claude Code / Codex、非対話)を起動し、`outputs` の
|
|
64
66
|
スキーマに適合する output を検証して報告する。出口が複数あれば AI が1つ選ぶ(複数出口=分岐)
|
|
65
67
|
- **文体の既定は「短く」**:出力の文章は承認画面・Slack・顧客への回答としてそのまま人が読むため、
|